feat: implement WebSocket heartbeat mechanism and apply API rate limiter

This commit is contained in:
2025-12-06 13:41:10 +01:00
parent eda43ff7d9
commit a74a009c10

View File

@@ -21,12 +21,19 @@ export class DashboardServer {
private server: ReturnType<typeof createServer> private server: ReturnType<typeof createServer>
private wss: WebSocketServer private wss: WebSocketServer
private clients: Set<WebSocket> = new Set() private clients: Set<WebSocket> = new Set()
private heartbeatInterval?: NodeJS.Timer
private dashboardLimiter = rateLimit({ private dashboardLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs for dashboard UI max: 100, // limit each IP to 100 requests per windowMs for dashboard UI
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
}) })
private apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 300, // reasonable cap for API interactions
standardHeaders: true,
legacyHeaders: false,
})
constructor() { constructor() {
this.app = express() this.app = express()
this.server = createServer(this.app) this.server = createServer(this.app)
@@ -67,7 +74,7 @@ export class DashboardServer {
} }
private setupRoutes(): void { private setupRoutes(): void {
this.app.use('/api', apiRouter) this.app.use('/api', this.apiLimiter, apiRouter)
// Health check // Health check
this.app.get('/health', (_req, res) => { this.app.get('/health', (_req, res) => {
@@ -107,15 +114,22 @@ export class DashboardServer {
private setupWebSocket(): void { private setupWebSocket(): void {
this.wss.on('connection', (ws: WebSocket) => { this.wss.on('connection', (ws: WebSocket) => {
this.clients.add(ws) const tracked = ws as WebSocket & { isAlive?: boolean }
tracked.isAlive = true
this.clients.add(tracked)
dashLog('WebSocket client connected') dashLog('WebSocket client connected')
ws.on('close', () => { tracked.on('pong', () => {
this.clients.delete(ws) tracked.isAlive = true
})
tracked.on('close', () => {
this.clients.delete(tracked)
dashLog('WebSocket client disconnected') dashLog('WebSocket client disconnected')
}) })
ws.on('error', (error) => { tracked.on('error', (error) => {
dashLog(`WebSocket error: ${error instanceof Error ? error.message : String(error)}`, 'error') dashLog(`WebSocket error: ${error instanceof Error ? error.message : String(error)}`, 'error')
}) })
@@ -124,7 +138,7 @@ export class DashboardServer {
const status = dashboardState.getStatus() const status = dashboardState.getStatus()
const accounts = dashboardState.getAccounts() const accounts = dashboardState.getAccounts()
ws.send(JSON.stringify({ tracked.send(JSON.stringify({
type: 'init', type: 'init',
data: { data: {
logs: recentLogs, logs: recentLogs,
@@ -133,6 +147,26 @@ export class DashboardServer {
} }
})) }))
}) })
// Heartbeat to drop dead connections and keep memory clean
this.heartbeatInterval = setInterval(() => {
for (const client of this.clients) {
const tracked = client as WebSocket & { isAlive?: boolean }
if (tracked.isAlive === false) {
tracked.terminate()
this.clients.delete(tracked)
continue
}
tracked.isAlive = false
try {
tracked.ping()
} catch (error) {
dashLog(`WebSocket ping error: ${error instanceof Error ? error.message : String(error)}`, 'error')
tracked.terminate()
this.clients.delete(tracked)
}
}
}, 30000)
} }
private interceptBotLogs(): void { private interceptBotLogs(): void {
@@ -192,6 +226,10 @@ export class DashboardServer {
} }
public stop(): void { public stop(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = undefined
}
this.wss.close() this.wss.close()
this.server.close() this.server.close()
dashLog('Server stopped') dashLog('Server stopped')