From 78629b9cb74387024756b36278a063bbed8cfced Mon Sep 17 00:00:00 2001 From: Elijah R Date: Thu, 11 Jul 2024 23:27:21 -0400 Subject: [PATCH] add chat and IP limiting --- server/config.example.toml | 11 ++++++++--- server/src/client.ts | 13 +++++++++++-- server/src/config.ts | 12 ++++++++++++ server/src/index.ts | 28 +++++++++++++++++++++++++++- server/src/ratelimiter.ts | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 server/src/ratelimiter.ts diff --git a/server/config.example.toml b/server/config.example.toml index 126c2f5..cc968f7 100644 --- a/server/config.example.toml +++ b/server/config.example.toml @@ -1,10 +1,15 @@ [http] host = "127.0.0.1" port = 3000 +proxied = true [chat] charlimit = 100 agentsDir = "agents/" +maxConnectionsPerIP = 2 + +[chat.ratelimits] +chat = {seconds = 10, limit = 8} [tts] enabled = true @@ -62,9 +67,9 @@ filename = "Office_Logo.ACS" friendlyName = "Peedy" filename = "Peedy.acs" -[[agents]] -friendlyName = "Question" -filename = "question_mark.acs" +#[[agents]] +#friendlyName = "Question" +#filename = "question_mark.acs" [[agents]] friendlyName = "Robby" diff --git a/server/src/client.ts b/server/src/client.ts index b0dec7d..f61ffd9 100644 --- a/server/src/client.ts +++ b/server/src/client.ts @@ -3,6 +3,7 @@ import { WebSocket } from "ws"; import { MSAgentJoinMessage, MSAgentProtocolMessage, MSAgentProtocolMessageType, MSAgentTalkMessage } from '@msagent-chat/protocol'; import { MSAgentChatRoom } from "./room.js"; import * as htmlentities from 'html-entities'; +import RateLimiter from "./ratelimiter.js"; // Event types @@ -15,17 +16,25 @@ export interface Client { } export class Client extends EventEmitter { + ip: string; username: string | null; agent: string | null; room: MSAgentChatRoom; socket: WebSocket; - constructor(socket: WebSocket, room: MSAgentChatRoom) { + + chatRateLimit: RateLimiter + + constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) { super(); this.socket = socket; + this.ip = ip; this.room = room; this.username = null; this.agent = null; + + this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat); + this.socket.on('message', (msg, isBinary) => { if (isBinary) { this.socket.close(); @@ -89,7 +98,7 @@ export class Client extends EventEmitter { } case MSAgentProtocolMessageType.Talk: { let talkMsg = msg as MSAgentTalkMessage; - if (!talkMsg.data || !talkMsg.data.msg) { + if (!talkMsg.data || !talkMsg.data.msg || !this.chatRateLimit.request()) { return; } if (talkMsg.data.msg.length > this.room.config.charlimit) return; diff --git a/server/src/config.ts b/server/src/config.ts index 745ae19..3918d87 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -2,6 +2,7 @@ export interface IConfig { http: { host: string; port: number; + proxied: boolean; } chat: ChatConfig; tts: TTSConfig; @@ -19,9 +20,20 @@ export interface TTSConfig { export interface ChatConfig { charlimit: number; agentsDir: string; + maxConnectionsPerIP: number; + ratelimits: { + chat: RateLimitConfig; + } } export interface AgentConfig { friendlyName: string; filename: string; +} + + + +export interface RateLimitConfig { + seconds: number; + limit: number; } \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 30f9443..143e83c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import { TTSClient } from './tts.js'; import path from 'path'; import { fileURLToPath } from 'url'; +import { isIP } from 'net'; let config: IConfig; let configPath: string; @@ -80,7 +81,32 @@ let room = new MSAgentChatRoom(config.chat, config.agents, tts); app.register(async app => { 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); }); }); diff --git a/server/src/ratelimiter.ts b/server/src/ratelimiter.ts new file mode 100644 index 0000000..8fd4877 --- /dev/null +++ b/server/src/ratelimiter.ts @@ -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; + } +} \ No newline at end of file