add chat and IP limiting

This commit is contained in:
Elijah R 2024-07-11 23:27:21 -04:00
parent bad5daf075
commit 78629b9cb7
5 changed files with 93 additions and 6 deletions

View file

@ -1,10 +1,15 @@
[http] [http]
host = "127.0.0.1" host = "127.0.0.1"
port = 3000 port = 3000
proxied = true
[chat] [chat]
charlimit = 100 charlimit = 100
agentsDir = "agents/" agentsDir = "agents/"
maxConnectionsPerIP = 2
[chat.ratelimits]
chat = {seconds = 10, limit = 8}
[tts] [tts]
enabled = true enabled = true
@ -62,9 +67,9 @@ filename = "Office_Logo.ACS"
friendlyName = "Peedy" friendlyName = "Peedy"
filename = "Peedy.acs" filename = "Peedy.acs"
[[agents]] #[[agents]]
friendlyName = "Question" #friendlyName = "Question"
filename = "question_mark.acs" #filename = "question_mark.acs"
[[agents]] [[agents]]
friendlyName = "Robby" friendlyName = "Robby"

View file

@ -3,6 +3,7 @@ import { WebSocket } from "ws";
import { MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol'; import { MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol';
import { MSAgentChatRoom } from "./room.js"; import { MSAgentChatRoom } from "./room.js";
import * as htmlentities from 'html-entities'; import * as htmlentities from 'html-entities';
import RateLimiter from "./ratelimiter.js";
// Event types // Event types
@ -15,17 +16,25 @@ export interface Client {
} }
export class Client extends EventEmitter { export class Client extends EventEmitter {
ip: string;
username: string | null; username: string | null;
agent: string | null; agent: string | null;
room: MSAgentChatRoom; room: MSAgentChatRoom;
socket: WebSocket; socket: WebSocket;
constructor(socket: WebSocket, room: MSAgentChatRoom) {
chatRateLimit: RateLimiter
constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) {
super(); super();
this.socket = socket; this.socket = socket;
this.ip = ip;
this.room = room; this.room = room;
this.username = null; this.username = null;
this.agent = null; this.agent = null;
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
this.socket.on('message', (msg, isBinary) => { this.socket.on('message', (msg, isBinary) => {
if (isBinary) { if (isBinary) {
this.socket.close(); this.socket.close();
@ -89,7 +98,7 @@ export class Client extends EventEmitter {
} }
case MSAgentProtocolMessageType.Talk: { case MSAgentProtocolMessageType.Talk: {
let talkMsg = msg as MSAgentTalkMessage; let talkMsg = msg as MSAgentTalkMessage;
if (!talkMsg.data || !talkMsg.data.msg) { if (!talkMsg.data || !talkMsg.data.msg || !this.chatRateLimit.request()) {
return; return;
} }
if (talkMsg.data.msg.length > this.room.config.charlimit) return; if (talkMsg.data.msg.length > this.room.config.charlimit) return;

View file

@ -2,6 +2,7 @@ export interface IConfig {
http: { http: {
host: string; host: string;
port: number; port: number;
proxied: boolean;
} }
chat: ChatConfig; chat: ChatConfig;
tts: TTSConfig; tts: TTSConfig;
@ -19,9 +20,20 @@ export interface TTSConfig {
export interface ChatConfig { export interface ChatConfig {
charlimit: number; charlimit: number;
agentsDir: string; agentsDir: string;
maxConnectionsPerIP: number;
ratelimits: {
chat: RateLimitConfig;
}
} }
export interface AgentConfig { export interface AgentConfig {
friendlyName: string; friendlyName: string;
filename: string; filename: string;
}
export interface RateLimitConfig {
seconds: number;
limit: number;
} }

View file

@ -9,6 +9,7 @@ import * as fs from 'fs';
import { TTSClient } from './tts.js'; import { TTSClient } from './tts.js';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { isIP } from 'net';
let config: IConfig; let config: IConfig;
let configPath: string; let configPath: string;
@ -80,7 +81,32 @@ let room = new MSAgentChatRoom(config.chat, config.agents, tts);
app.register(async app => { app.register(async app => {
app.get("/api/socket", {websocket: true}, (socket, req) => { app.get("/api/socket", {websocket: true}, (socket, req) => {
let client = new Client(socket, room); // TODO: Do this pre-upgrade and return the appropriate status codes
let ip: string;
if (config.http.proxied) {
if (req.headers["x-forwarded-for"] === undefined) {
console.error(`Warning: X-Forwarded-For not set! This is likely a misconfiguration of your reverse proxy.`);
socket.close();
return;
}
let xff = req.headers["x-forwarded-for"];
if (xff instanceof Array)
ip = xff[0];
else
ip = xff;
if (!isIP(ip)) {
console.error(`Warning: X-Forwarded-For malformed! This is likely a misconfiguration of your reverse proxy.`);
socket.close();
return;
}
} else {
ip = req.ip;
}
let o = room.clients.filter(c => c.ip === ip);
if (o.length >= config.chat.maxConnectionsPerIP) {
o[0].socket.close();
}
let client = new Client(socket, room, ip);
room.addClient(client); room.addClient(client);
}); });
}); });

35
server/src/ratelimiter.ts Normal file
View file

@ -0,0 +1,35 @@
import { EventEmitter } from 'events';
import { RateLimitConfig } from './config';
// Class to ratelimit a resource (chatting, logging in, etc)
export default class RateLimiter extends EventEmitter {
private limit: number;
private interval: number;
private requestCount: number;
private limiter?: NodeJS.Timeout;
private limiterSet: boolean;
constructor(config: RateLimitConfig) {
super();
this.limit = config.limit;
this.interval = config.seconds;
this.requestCount = 0;
this.limiterSet = false;
}
// Return value is whether or not the action should be continued
request(): boolean {
this.requestCount++;
if (this.requestCount >= this.limit) {
this.emit('limit');
return false;
}
if (!this.limiterSet) {
this.limiter = setTimeout(() => {
this.limiterSet = false;
this.requestCount = 0;
}, this.interval * 1000);
this.limiterSet = true;
}
return true;
}
}