bb-inspired
Version:
Core library for BB-inspired NestJS backend
210 lines • 7.91 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var WebsocketRateLimiter_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebsocketRateLimiter = void 0;
const common_1 = require("@nestjs/common");
const logger_1 = require("../../utils/logger");
let WebsocketRateLimiter = WebsocketRateLimiter_1 = class WebsocketRateLimiter {
constructor(options = {}) {
this.options = options;
this.logger = new logger_1.AppLogger(WebsocketRateLimiter_1.name);
this.ipConnections = new Map();
this.clientMessages = new Map();
this.clientSubscriptions = new Map();
this.blockedIps = new Map();
this.ipViolations = new Map();
this.bannedIps = new Map();
this.options = {
maxConnectionsPerIp: 10,
maxMessagesPerMinute: 120,
maxSubscriptionsPerClient: 30,
blockDuration: 5 * 60 * 1000,
autoDisconnect: true,
enableBanning: true,
banThreshold: 5,
banDuration: 24 * 60 * 60 * 1000,
...options
};
setInterval(() => this.cleanup(), 60 * 1000);
setInterval(() => this.resetMessageCounters(), 60 * 1000);
}
isBanned(ip) {
if (!this.bannedIps.has(ip)) {
return false;
}
const banExpiry = this.bannedIps.get(ip);
if (Date.now() > banExpiry) {
this.bannedIps.delete(ip);
return false;
}
return true;
}
isBlocked(ip) {
if (!this.blockedIps.has(ip)) {
return false;
}
const blockExpiry = this.blockedIps.get(ip);
if (Date.now() > blockExpiry) {
this.blockedIps.delete(ip);
return false;
}
return true;
}
handleConnection(client) {
const ip = this.getClientIp(client);
if (this.isBanned(ip)) {
this.logger.warn(`Rejected connection from banned IP: ${ip}`);
return false;
}
if (this.isBlocked(ip)) {
this.logger.warn(`Rejected connection from blocked IP: ${ip}`);
return false;
}
const currentConnections = this.ipConnections.get(ip) || 0;
if (currentConnections >= this.options.maxConnectionsPerIp) {
this.logger.warn(`Connection limit exceeded for IP: ${ip}`);
this.recordViolation(ip);
return false;
}
this.ipConnections.set(ip, currentConnections + 1);
this.clientMessages.set(client.id, {
count: 0,
reset: Date.now() + 60 * 1000
});
this.clientSubscriptions.set(client.id, new Set());
return true;
}
handleDisconnect(client) {
const ip = this.getClientIp(client);
const currentConnections = this.ipConnections.get(ip) || 0;
if (currentConnections > 0) {
this.ipConnections.set(ip, currentConnections - 1);
}
this.clientMessages.delete(client.id);
this.clientSubscriptions.delete(client.id);
}
handleMessage(client) {
const tracking = this.clientMessages.get(client.id);
if (!tracking) {
this.clientMessages.set(client.id, {
count: 1,
reset: Date.now() + 60 * 1000
});
return true;
}
if (Date.now() > tracking.reset) {
tracking.count = 1;
tracking.reset = Date.now() + 60 * 1000;
return true;
}
if (tracking.count >= this.options.maxMessagesPerMinute) {
this.logger.warn(`Message rate limit exceeded for client: ${client.id}`);
this.recordViolation(this.getClientIp(client));
if (this.options.autoDisconnect) {
client.disconnect(true);
}
return false;
}
tracking.count++;
return true;
}
handleSubscription(client, channel) {
const subscriptions = this.clientSubscriptions.get(client.id);
if (!subscriptions) {
const newSet = new Set([channel]);
this.clientSubscriptions.set(client.id, newSet);
return true;
}
if (subscriptions.has(channel)) {
return true;
}
if (subscriptions.size >= this.options.maxSubscriptionsPerClient) {
this.logger.warn(`Subscription limit exceeded for client: ${client.id}`);
return false;
}
subscriptions.add(channel);
return true;
}
handleUnsubscription(client, channel) {
const subscriptions = this.clientSubscriptions.get(client.id);
if (subscriptions) {
subscriptions.delete(channel);
}
}
blockIp(ip) {
this.blockedIps.set(ip, Date.now() + this.options.blockDuration);
this.logger.warn(`Blocked IP ${ip} for ${this.options.blockDuration / 1000} seconds`);
}
banIp(ip) {
this.bannedIps.set(ip, Date.now() + this.options.banDuration);
this.logger.warn(`Banned IP ${ip} for ${this.options.banDuration / (1000 * 60 * 60)} hours`);
}
recordViolation(ip) {
this.blockIp(ip);
if (!this.options.enableBanning) {
return;
}
const violations = this.ipViolations.get(ip) || { count: 0, lastViolation: 0 };
if (Date.now() - violations.lastViolation > 24 * 60 * 60 * 1000) {
violations.count = 1;
}
else {
violations.count++;
}
violations.lastViolation = Date.now();
this.ipViolations.set(ip, violations);
if (violations.count >= this.options.banThreshold) {
this.banIp(ip);
this.ipViolations.delete(ip);
}
}
resetMessageCounters() {
const now = Date.now();
for (const [clientId, tracking] of this.clientMessages.entries()) {
if (now > tracking.reset) {
tracking.count = 0;
tracking.reset = now + 60 * 1000;
}
}
}
cleanup() {
const now = Date.now();
for (const [ip, expiry] of this.blockedIps.entries()) {
if (now > expiry) {
this.blockedIps.delete(ip);
}
}
for (const [ip, expiry] of this.bannedIps.entries()) {
if (now > expiry) {
this.bannedIps.delete(ip);
}
}
for (const [ip, violation] of this.ipViolations.entries()) {
if (now - violation.lastViolation > 24 * 60 * 60 * 1000) {
this.ipViolations.delete(ip);
}
}
}
getClientIp(client) {
const xForwardedFor = client.handshake.headers['x-forwarded-for'];
if (xForwardedFor) {
return xForwardedFor.split(',')[0].trim();
}
return client.handshake.address;
}
};
exports.WebsocketRateLimiter = WebsocketRateLimiter;
exports.WebsocketRateLimiter = WebsocketRateLimiter = WebsocketRateLimiter_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [Object])
], WebsocketRateLimiter);
//# sourceMappingURL=websocket.rate-limiter.js.map