89 lines
2.4 KiB
TypeScript
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
|
|
);
|
|
}
|