ai-chatbot/lib/security/manual-ip-blocklist.ts

89 lines
2.4 KiB
TypeScript

import "server-only";
import { isIP } from "node:net";
export type ManualIpBlockEntry = {
id: string;
ip: string;
reason?: string | null;
expiresAt?: Date | string | null;
};
function ipv4ToNumber(value: string) {
const parts = value.split(".").map((part) => Number.parseInt(part, 10));
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part))) {
return null;
}
if (parts.some((part) => part < 0 || part > 255)) {
return null;
}
return (
((parts[0] << 24) >>> 0) +
((parts[1] << 16) >>> 0) +
((parts[2] << 8) >>> 0) +
(parts[3] >>> 0)
) >>> 0;
}
function normalizePlainIp(value: string) {
const normalized = value.trim().replace(/^\[/, "").replace(/\]$/, "");
return isIP(normalized) ? normalized : null;
}
function normalizeIpv4Cidr(value: string) {
const [rawIp, rawPrefix] = value.split("/");
const ip = normalizePlainIp(rawIp || "");
const prefix = Number.parseInt(rawPrefix || "", 10);
if (!ip || isIP(ip) !== 4) return null;
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return null;
return `${ip}/${prefix}`;
}
export function normalizeManualBlockIp(input: unknown) {
const normalized = String(input || "").trim();
if (!normalized) return null;
if (normalized.includes("/")) {
return normalizeIpv4Cidr(normalized);
}
return normalizePlainIp(normalized);
}
function ipv4MatchesCidr(ip: string, cidr: string) {
const [rangeIp, rawPrefix] = cidr.split("/");
const ipNumber = ipv4ToNumber(ip);
const rangeNumber = ipv4ToNumber(rangeIp || "");
const prefix = Number.parseInt(rawPrefix || "", 10);
if (ipNumber == null || rangeNumber == null) return false;
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
return (ipNumber & mask) === (rangeNumber & mask);
}
export function matchManualIpBlock(
ip: string | null | undefined,
entries: ManualIpBlockEntry[]
) {
const normalizedIp = normalizePlainIp(String(ip || ""));
if (!normalizedIp) return null;
const ipVersion = isIP(normalizedIp);
return (
entries.find((entry) => {
const rule = normalizeManualBlockIp(entry.ip);
if (!rule) return false;
if (rule.includes("/")) {
return ipVersion === 4 && ipv4MatchesCidr(normalizedIp, rule);
}
return rule.toLowerCase() === normalizedIp.toLowerCase();
}) || null
);
}