add chat and IP limiting
This commit is contained in:
parent
bad5daf075
commit
78629b9cb7
5 changed files with 93 additions and 6 deletions
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
35
server/src/ratelimiter.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue