feat : 6 astra improve it
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { isIP } from "node:net";
|
||||
|
||||
import { getRedisClient } from "@/lib/redis/client";
|
||||
import { HttpError } from "./http";
|
||||
|
||||
const consumeScript = `
|
||||
local count = redis.call('INCR', KEYS[1])
|
||||
if count == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[1]) end
|
||||
local ttl = redis.call('PTTL', KEYS[1])
|
||||
if ttl < 0 then redis.call('PEXPIRE', KEYS[1], ARGV[1]); ttl = tonumber(ARGV[1]) end
|
||||
return {count, ttl}
|
||||
`;
|
||||
|
||||
export function trustedClientAddress(headers: Headers): string {
|
||||
// Only enable this after the origin is restricted to the sanitizing proxy.
|
||||
const header = process.env.TRUSTED_CLIENT_IP_HEADER;
|
||||
const address = header ? headers.get(header)?.trim() : undefined;
|
||||
if (!address || address.includes("%") || !isIP(address)) return "unknown";
|
||||
if (isIP(address) === 4) return address;
|
||||
// URL normalizes alternate IPv6 spellings. Collapse residential /64s.
|
||||
const normalized = new URL(`http://[${address}]/`).hostname.slice(1, -1);
|
||||
if (normalized.startsWith("::ffff:")) {
|
||||
const groups = normalized.slice(7).split(":").map((part) => parseInt(part, 16));
|
||||
return `${groups[0] >> 8}.${groups[0] & 255}.${groups[1] >> 8}.${groups[1] & 255}`;
|
||||
}
|
||||
const [left, right = ""] = normalized.split("::");
|
||||
const first = left ? left.split(":") : [];
|
||||
const last = right ? right.split(":") : [];
|
||||
const groups = [...first, ...Array(8 - first.length - last.length).fill("0"), ...last];
|
||||
return `${groups.slice(0, 4).join(":")}/64`;
|
||||
}
|
||||
|
||||
export async function consumeRateLimit(key: string, rule: { window: number; max: number }) {
|
||||
const digest = createHash("sha256").update(key).digest("hex");
|
||||
const redis = await getRedisClient();
|
||||
const result = await redis.eval(consumeScript, 1,
|
||||
`${process.env.REDIS_SECURITY_PREFIX || "buzz:security"}:rate:${digest}`, rule.window * 1000) as [number, number];
|
||||
return { allowed: result[0] <= rule.max, retryAfter: result[0] <= rule.max ? null : Math.max(1, Math.ceil(result[1] / 1000)) };
|
||||
}
|
||||
|
||||
export async function limitRequest(scope: string, identity: string, max: number, window = 60) {
|
||||
const result = await consumeRateLimit(`${scope}:${identity}`, { window, max });
|
||||
if (!result.allowed) throw new HttpError(429, "too-many-requests", result.retryAfter ?? window);
|
||||
}
|
||||
Reference in New Issue
Block a user