install

npm install lru-cache

Code

import Koa, { Context, Next } from "koa";
import { LRUCache } from "lru-cache";
import { Debug } from "../util";

const THRESHOLD_MS_DANGEROUS_INTERVAL = 500; // 이 시간보다 짧게 연속 요청하면 위험한 것으로 간주
const THRESHOLD_CNT_CONTINUOUS_REQUEST = 50; // 이 횟수 이상으로 연속 요청하면 위험한 것으로 간주
const BLOCK_MS = 5_000; // 차단 유지 시간(ms)

interface ClientInfo {
    lastRequestTime: number;
    warnings: number;
    blockStartTime?: number;
}

class RateLimiter {
    private cache: LRUCache<string, ClientInfo>;

    constructor() {
        this.cache = new LRUCache<string, ClientInfo>({
            max: 10, // 최대 10개의 클라이언트 IP를 저장
            ttl: 1000 * 60 * 60 // 1시간 후에 정보가 만료됩니다 (필요에 따라 조정)
        });
    }

    public handleRequest(ctx: Context, next: Next) {
        const clientIp = ctx.ip;
        const currentTime = Date.now();
        let clientInfo = this.cache.get(clientIp);

        if (clientInfo) {
            const timeDiff = currentTime - clientInfo.lastRequestTime;

            if (timeDiff < THRESHOLD_MS_DANGEROUS_INTERVAL) { // 500ms 이내의 요청은 경고 카운트 증가
                clientInfo.warnings += 1;
            }

            // 경고 카운트 체크하여 차단
            if (clientInfo.warnings >= THRESHOLD_CNT_CONTINUOUS_REQUEST) {
                if (clientInfo.warnings === THRESHOLD_CNT_CONTINUOUS_REQUEST) {
                    Debug.TLogError(`BLOCKED: ${clientIp}`);
                }
                if (!clientInfo.blockStartTime) {
                    clientInfo.blockStartTime = currentTime;
                }

                if (currentTime - clientInfo.blockStartTime < BLOCK_MS) { // 지정 시간만큼 차단
                    ctx.status = 403;
                    return;
                } else {
                    // 차단 시간이 지난 후 초기화
                    clientInfo.warnings = 0;
                    clientInfo.blockStartTime = undefined;
                }
            }

            clientInfo.lastRequestTime = currentTime;
            this.cache.set(clientIp, clientInfo); // 업데이트된 정보 저장
        } else {
            // 새 클라이언트 정보 추가
            this.cache.set(clientIp, { lastRequestTime: currentTime, warnings: 0 });
            Debug.TLog(`New Client: ${clientIp}`);
        }

        return next();
    }
}

export function applyRateLimiter(app: Koa) {
    const rateLimiter = new RateLimiter();
    app.use((ctx, next) => rateLimiter.handleRequest(ctx, next));
}