376 lines
25 KiB
JavaScript
376 lines
25 KiB
JavaScript
import { readFileSync, writeFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { logger, securityLogger } from '../commons/logger.js';
|
|
// Configuration
|
|
const BAN_THRESHOLD = parseInt(process.env.AUTO_BAN_THRESHOLD || '5', 10); // Number of violations before ban
|
|
const VIOLATION_WINDOW_MS = parseInt(process.env.AUTO_BAN_WINDOW_MS || '10000', 10); // 1 minute default
|
|
const VIOLATION_CLEANUP_INTERVAL = 10000; // Clean up old violations every minute
|
|
console.log('Auto-ban configured with:', {
|
|
threshold: BAN_THRESHOLD,
|
|
window: VIOLATION_WINDOW_MS / 60000,
|
|
cleanupInterval: VIOLATION_CLEANUP_INTERVAL / 60000
|
|
});
|
|
// In-memory violation tracking
|
|
const violations = new Map();
|
|
let banList = {
|
|
bannedIPs: [],
|
|
bannedUserIds: [],
|
|
bannedTokens: [],
|
|
};
|
|
/**
|
|
* Load ban list from JSON file
|
|
*/
|
|
export function loadBanList() {
|
|
try {
|
|
const banListPath = join(process.cwd(), 'config', 'ban.json');
|
|
const data = readFileSync(banListPath, 'utf-8');
|
|
banList = JSON.parse(data);
|
|
return banList;
|
|
}
|
|
catch (error) {
|
|
logger.error({ error }, 'Failed to load ban list');
|
|
return banList;
|
|
}
|
|
}
|
|
/**
|
|
* Save ban list to JSON file
|
|
*/
|
|
function saveBanList() {
|
|
try {
|
|
const banListPath = join(process.cwd(), 'config', 'ban.json');
|
|
writeFileSync(banListPath, JSON.stringify(banList, null, 4), 'utf-8');
|
|
logger.info('Ban list saved');
|
|
}
|
|
catch (error) {
|
|
logger.error({ error }, 'Failed to save ban list');
|
|
}
|
|
}
|
|
/**
|
|
* Get current ban list
|
|
*/
|
|
export function getBanList() {
|
|
return banList;
|
|
}
|
|
/**
|
|
* Check if an IP is banned
|
|
*/
|
|
export function isIPBanned(ip) {
|
|
return banList.bannedIPs.includes(ip);
|
|
}
|
|
/**
|
|
* Check if a user ID is banned
|
|
*/
|
|
export function isUserBanned(userId) {
|
|
return banList.bannedUserIds.includes(userId);
|
|
}
|
|
/**
|
|
* Check if an auth token is banned
|
|
*/
|
|
export function isTokenBanned(token) {
|
|
return banList.bannedTokens.includes(token);
|
|
}
|
|
/**
|
|
* Extract IP address from request
|
|
*/
|
|
export function getClientIP(c) {
|
|
// Check forwarded headers first (for proxies)
|
|
const forwarded = c.req.header('x-forwarded-for');
|
|
if (forwarded) {
|
|
return forwarded.split(',')[0].trim();
|
|
}
|
|
const realIp = c.req.header('x-real-ip');
|
|
if (realIp) {
|
|
return realIp;
|
|
}
|
|
// Fallback to connection IP (works for localhost)
|
|
// In Node.js/Hono, we can try to get the remote address
|
|
try {
|
|
// @ts-ignore - accessing internal request object
|
|
const remoteAddress = c.req.raw?.socket?.remoteAddress || c.env?.ip;
|
|
if (remoteAddress) {
|
|
return remoteAddress;
|
|
}
|
|
}
|
|
catch (e) {
|
|
// Ignore errors
|
|
}
|
|
// Last resort: use localhost identifier
|
|
return '127.0.0.1';
|
|
}
|
|
/**
|
|
* Extract user ID from authorization header
|
|
*/
|
|
function getUserId(c) {
|
|
const authHeader = c.req.header('authorization');
|
|
if (!authHeader)
|
|
return null;
|
|
return authHeader;
|
|
}
|
|
/**
|
|
* Record a rate limit violation
|
|
*/
|
|
export function recordViolation(key) {
|
|
const now = Date.now();
|
|
const existing = violations.get(key);
|
|
if (existing) {
|
|
// Check if violation is within the window
|
|
if (now - existing.firstViolation <= VIOLATION_WINDOW_MS) {
|
|
existing.count++;
|
|
existing.lastViolation = now;
|
|
violations.set(key, existing);
|
|
// Check if threshold exceeded
|
|
if (existing.count >= BAN_THRESHOLD) {
|
|
banEntity(key);
|
|
}
|
|
}
|
|
else {
|
|
// Reset violation count if outside window
|
|
violations.set(key, {
|
|
count: 1,
|
|
firstViolation: now,
|
|
lastViolation: now,
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// First violation
|
|
violations.set(key, {
|
|
count: 1,
|
|
firstViolation: now,
|
|
lastViolation: now,
|
|
});
|
|
}
|
|
logger.debug({ key, violations: violations.get(key) }, 'Violation recorded');
|
|
}
|
|
/**
|
|
* Ban an entity (IP, user, or token)
|
|
*/
|
|
function banEntity(key) {
|
|
const [type, value] = key.split(':', 2);
|
|
const violationRecord = violations.get(key);
|
|
let added = false;
|
|
if (type === 'ip' && !banList.bannedIPs.includes(value)) {
|
|
banList.bannedIPs.push(value);
|
|
added = true;
|
|
// Log to security.json
|
|
securityLogger.warn({
|
|
event: 'auto_ban',
|
|
type: 'ip',
|
|
ip: value,
|
|
violations: violationRecord?.count,
|
|
firstViolation: violationRecord?.firstViolation,
|
|
lastViolation: violationRecord?.lastViolation
|
|
}, 'IP auto-banned for excessive requests');
|
|
// Also log to console
|
|
logger.info({ ip: value, violations: violationRecord?.count }, '🚫 IP auto-banned for excessive requests');
|
|
}
|
|
else if (type === 'user' && !banList.bannedUserIds.includes(value)) {
|
|
banList.bannedUserIds.push(value);
|
|
added = true;
|
|
// Log to security.json
|
|
securityLogger.warn({
|
|
event: 'auto_ban',
|
|
type: 'user',
|
|
userId: value,
|
|
violations: violationRecord?.count,
|
|
firstViolation: violationRecord?.firstViolation,
|
|
lastViolation: violationRecord?.lastViolation
|
|
}, 'User auto-banned for excessive requests');
|
|
// Also log to console
|
|
logger.info({ userId: value, violations: violationRecord?.count }, '🚫 User auto-banned for excessive requests');
|
|
}
|
|
else if (type === 'token' && !banList.bannedTokens.includes(value)) {
|
|
banList.bannedTokens.push(value);
|
|
added = true;
|
|
// Log to security.json
|
|
securityLogger.warn({
|
|
event: 'auto_ban',
|
|
type: 'token',
|
|
token: value.substring(0, 20) + '...',
|
|
violations: violationRecord?.count,
|
|
firstViolation: violationRecord?.firstViolation,
|
|
lastViolation: violationRecord?.lastViolation
|
|
}, 'Token auto-banned for excessive requests');
|
|
// Also log to console
|
|
logger.info({ token: value.substring(0, 20) + '...', violations: violationRecord?.count }, '🚫 Token auto-banned for excessive requests');
|
|
}
|
|
if (added) {
|
|
saveBanList();
|
|
// Clear violation record after ban
|
|
violations.delete(key);
|
|
}
|
|
}
|
|
/**
|
|
* Clean up old violation records
|
|
*/
|
|
function cleanupViolations() {
|
|
const now = Date.now();
|
|
let cleaned = 0;
|
|
for (const [key, record] of violations.entries()) {
|
|
if (now - record.lastViolation > VIOLATION_WINDOW_MS) {
|
|
violations.delete(key);
|
|
cleaned++;
|
|
}
|
|
}
|
|
if (cleaned > 0) {
|
|
logger.debug({ cleaned }, 'Cleaned up old violation records');
|
|
}
|
|
}
|
|
/**
|
|
* Auto-ban middleware
|
|
* Checks if request is from a banned entity
|
|
*/
|
|
// Simple in-memory rate limiting
|
|
const requestCounts = new Map();
|
|
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '20', 10);
|
|
const RATE_LIMIT_WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '1000', 10);
|
|
export async function autoBanMiddleware(c, next) {
|
|
const ip = getClientIP(c);
|
|
const path = c.req.path;
|
|
const method = c.req.method;
|
|
// Skip ban/rate-limit checks for local requests (dev & e2e tests)
|
|
if (ip === '127.0.0.1' || ip === 'localhost' || ip === '::1' || ip === '::ffff:127.0.0.1') {
|
|
return next();
|
|
}
|
|
const authHeader = c.req.header('authorization');
|
|
const userId = getUserId(c);
|
|
// Generate key for rate limiting
|
|
let key;
|
|
if (authHeader) {
|
|
key = `user:${authHeader}`;
|
|
}
|
|
else {
|
|
key = `ip:${ip}`;
|
|
}
|
|
// Check if IP is banned
|
|
if (isIPBanned(ip)) {
|
|
/*
|
|
securityLogger.info({
|
|
event: 'blocked_request',
|
|
type: 'ip',
|
|
ip,
|
|
path,
|
|
method
|
|
}, 'Blocked request from banned IP')
|
|
*/
|
|
// logger.info({ ip, path }, '🚫 Blocked request from banned IP')
|
|
return c.json({
|
|
error: 'Forbidden',
|
|
message: 'Your IP address has been banned for excessive requests',
|
|
}, 403);
|
|
}
|
|
// Check if auth token is banned
|
|
if (authHeader && isTokenBanned(authHeader)) {
|
|
securityLogger.info({
|
|
event: 'blocked_request',
|
|
type: 'token',
|
|
token: authHeader.substring(0, 20) + '...',
|
|
path,
|
|
method
|
|
}, 'Blocked request from banned token');
|
|
logger.info({ token: authHeader.substring(0, 20) + '...', path }, '🚫 Blocked request from banned token');
|
|
return c.json({
|
|
error: 'Forbidden',
|
|
message: 'Your access token has been banned for excessive requests',
|
|
}, 403);
|
|
}
|
|
// Check if user ID is banned
|
|
if (userId && isUserBanned(userId)) {
|
|
securityLogger.info({
|
|
event: 'blocked_request',
|
|
type: 'user',
|
|
userId,
|
|
path,
|
|
method
|
|
}, 'Blocked request from banned user');
|
|
logger.info({ userId, path }, '🚫 Blocked request from banned user');
|
|
return c.json({
|
|
error: 'Forbidden',
|
|
message: 'Your account has been banned for excessive requests',
|
|
}, 403);
|
|
}
|
|
// Built-in rate limiting (since hono-rate-limiter isn't working)
|
|
const now = Date.now();
|
|
const record = requestCounts.get(key);
|
|
if (record) {
|
|
if (now < record.resetTime) {
|
|
// Within the window
|
|
record.count++;
|
|
if (record.count > RATE_LIMIT_MAX) {
|
|
// Rate limit exceeded!
|
|
console.log(`⚠️ Rate limit exceeded for ${key} (${record.count}/${RATE_LIMIT_MAX})`);
|
|
recordViolation(key);
|
|
return c.json({
|
|
error: 'Too many requests',
|
|
message: `Rate limit exceeded. Maximum ${RATE_LIMIT_MAX} requests per ${RATE_LIMIT_WINDOW_MS}ms`,
|
|
}, 429);
|
|
}
|
|
}
|
|
else {
|
|
// Window expired, reset
|
|
record.count = 1;
|
|
record.resetTime = now + RATE_LIMIT_WINDOW_MS;
|
|
}
|
|
}
|
|
else {
|
|
// First request
|
|
requestCounts.set(key, {
|
|
count: 1,
|
|
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
});
|
|
}
|
|
await next();
|
|
}
|
|
/**
|
|
* Manually unban an IP
|
|
*/
|
|
export function unbanIP(ip) {
|
|
const index = banList.bannedIPs.indexOf(ip);
|
|
if (index > -1) {
|
|
banList.bannedIPs.splice(index, 1);
|
|
saveBanList();
|
|
securityLogger.info({
|
|
event: 'unban',
|
|
type: 'ip',
|
|
ip
|
|
}, 'IP unbanned');
|
|
logger.info({ ip }, 'IP unbanned');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Manually unban a user
|
|
*/
|
|
export function unbanUser(userId) {
|
|
const index = banList.bannedUserIds.indexOf(userId);
|
|
if (index > -1) {
|
|
banList.bannedUserIds.splice(index, 1);
|
|
saveBanList();
|
|
securityLogger.info({
|
|
event: 'unban',
|
|
type: 'user',
|
|
userId
|
|
}, 'User unbanned');
|
|
logger.info({ userId }, 'User unbanned');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Get current violation stats
|
|
*/
|
|
export function getViolationStats() {
|
|
return {
|
|
totalViolations: violations.size,
|
|
violations: Array.from(violations.entries()).map(([key, record]) => ({
|
|
key,
|
|
...record,
|
|
})),
|
|
};
|
|
}
|
|
// Load ban list on module initialization
|
|
loadBanList();
|
|
// Start cleanup interval
|
|
setInterval(cleanupViolations, VIOLATION_CLEANUP_INTERVAL);
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0b0Jhbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9taWRkbGV3YXJlL2F1dG9CYW4udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLFlBQVksRUFBRSxhQUFhLEVBQUUsTUFBTSxJQUFJLENBQUE7QUFDaEQsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLE1BQU0sQ0FBQTtBQUMzQixPQUFPLEVBQUUsTUFBTSxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFBO0FBYzdELGdCQUFnQjtBQUNoQixNQUFNLGFBQWEsR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsSUFBSSxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUEsQ0FBQyxrQ0FBa0M7QUFDNUcsTUFBTSxtQkFBbUIsR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsSUFBSSxPQUFPLEVBQUUsRUFBRSxDQUFDLENBQUEsQ0FBQyxtQkFBbUI7QUFDdkcsTUFBTSwwQkFBMEIsR0FBRyxLQUFLLENBQUEsQ0FBQyx1Q0FBdUM7QUFFaEYsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQkFBMkIsRUFBRTtJQUNyQyxTQUFTLEVBQUUsYUFBYTtJQUN4QixNQUFNLEVBQUUsbUJBQW1CLEdBQUcsS0FBSztJQUNuQyxlQUFlLEVBQUUsMEJBQTBCLEdBQUcsS0FBSztDQUN0RCxDQUFDLENBQUE7QUFFRiwrQkFBK0I7QUFDL0IsTUFBTSxVQUFVLEdBQUcsSUFBSSxHQUFHLEVBQTJCLENBQUE7QUFFckQsSUFBSSxPQUFPLEdBQVk7SUFDbkIsU0FBUyxFQUFFLEVBQUU7SUFDYixhQUFhLEVBQUUsRUFBRTtJQUNqQixZQUFZLEVBQUUsRUFBRTtDQUNuQixDQUFBO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLFVBQVUsV0FBVztJQUN2QixJQUFJLENBQUM7UUFDRCxNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLEdB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
|