mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
180 lines
5.0 KiB
TypeScript
180 lines
5.0 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {isIP} from 'node:net';
|
|
import {type IpAddressFamily, ipToBytes, normalizeIpString} from '@fluxer/ip_utils/src/IpAddress';
|
|
|
|
interface ParsedBase {
|
|
canonical: string;
|
|
family: IpAddressFamily;
|
|
}
|
|
|
|
interface ParsedIpSingle extends ParsedBase {
|
|
type: 'single';
|
|
value: bigint;
|
|
}
|
|
|
|
interface ParsedIpRange extends ParsedBase {
|
|
type: 'range';
|
|
prefixLength: number;
|
|
start: bigint;
|
|
end: bigint;
|
|
}
|
|
|
|
type IpBanEntry = ParsedIpSingle | ParsedIpRange;
|
|
|
|
const BIT_LENGTHS: Record<IpAddressFamily, number> = {
|
|
ipv4: 32,
|
|
ipv6: 128,
|
|
};
|
|
|
|
function bufferToBigInt(bytes: Buffer): bigint {
|
|
let value = 0n;
|
|
for (const byte of bytes) {
|
|
value = (value << 8n) | BigInt(byte);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function formatNormalizedAddress(family: IpAddressFamily, bytes: Buffer): string {
|
|
if (family === 'ipv4') {
|
|
return Array.from(bytes.values()).join('.');
|
|
}
|
|
const groups: Array<string> = [];
|
|
for (let i = 0; i < 16; i += 2) {
|
|
const high = bytes[i];
|
|
const low = bytes[i + 1];
|
|
const value = (high << 8) | low;
|
|
groups.push(value.toString(16).padStart(4, '0'));
|
|
}
|
|
return groups.join(':');
|
|
}
|
|
|
|
interface ParsedIP {
|
|
family: IpAddressFamily;
|
|
bytes: Buffer;
|
|
mappedIpv4Bytes: Buffer | null;
|
|
}
|
|
|
|
interface ParsedCIDR {
|
|
family: IpAddressFamily;
|
|
address: Buffer;
|
|
prefixLength: number;
|
|
}
|
|
|
|
function parseRange(value: string): ParsedIpRange | null {
|
|
const normalized = normalizeIpString(value);
|
|
if (!normalized) return null;
|
|
const parsed = parseCIDR(normalized);
|
|
if (!parsed) return null;
|
|
const {family, address, prefixLength} = parsed;
|
|
const totalBits = BIT_LENGTHS[family];
|
|
const hostBits = totalBits - prefixLength;
|
|
const baseValue = bufferToBigInt(address);
|
|
const mask = hostBits === 0 ? 0n : (1n << BigInt(hostBits)) - 1n;
|
|
const start = baseValue & ~mask;
|
|
const end = baseValue | mask;
|
|
return {
|
|
type: 'range',
|
|
family,
|
|
canonical: `${formatNormalizedAddress(family, address)}/${prefixLength}`,
|
|
prefixLength,
|
|
start,
|
|
end,
|
|
};
|
|
}
|
|
|
|
function parseSingle(value: string): ParsedIpSingle | null {
|
|
const normalized = normalizeIpString(value);
|
|
if (!normalized || normalized.includes('/')) return null;
|
|
const parsed = parseIP(normalized);
|
|
if (!parsed) return null;
|
|
const resolved = resolveMappedIpv4(parsed);
|
|
return {
|
|
type: 'single',
|
|
family: resolved.family,
|
|
canonical: formatNormalizedAddress(resolved.family, resolved.bytes),
|
|
value: bufferToBigInt(resolved.bytes),
|
|
};
|
|
}
|
|
|
|
function parseIP(value: string): ParsedIP | null {
|
|
const ipWithoutZone = value.split('%', 1)[0].trim();
|
|
if (!ipWithoutZone) return null;
|
|
if (!isIP(ipWithoutZone)) return null;
|
|
const parsed = ipToBytes(ipWithoutZone);
|
|
if (!parsed) return null;
|
|
const bytes = Buffer.from(parsed.bytes);
|
|
return {family: parsed.family, bytes, mappedIpv4Bytes: parsed.family === 'ipv6' ? extractMappedIpv4(bytes) : null};
|
|
}
|
|
|
|
function resolveMappedIpv4(parsed: ParsedIP): {
|
|
family: IpAddressFamily;
|
|
bytes: Buffer;
|
|
} {
|
|
if (parsed.family === 'ipv6' && parsed.mappedIpv4Bytes) {
|
|
return {family: 'ipv4', bytes: parsed.mappedIpv4Bytes};
|
|
}
|
|
return {family: parsed.family, bytes: parsed.bytes};
|
|
}
|
|
|
|
function extractMappedIpv4(bytes: Buffer): Buffer | null {
|
|
if (bytes.length !== 16) return null;
|
|
for (let i = 0; i < 10; i++) {
|
|
if (bytes[i] !== 0) return null;
|
|
}
|
|
if (bytes[10] !== 0xff || bytes[11] !== 0xff) return null;
|
|
return Buffer.from(bytes.subarray(12, 16));
|
|
}
|
|
|
|
function parseCIDR(cidr: string): ParsedCIDR | null {
|
|
const trimmed = cidr.trim();
|
|
if (!trimmed) return null;
|
|
const slashIdx = trimmed.indexOf('/');
|
|
if (slashIdx === -1) return null;
|
|
const ipPart = trimmed.slice(0, slashIdx).trim();
|
|
const prefixPart = trimmed.slice(slashIdx + 1).trim();
|
|
if (!ipPart || !prefixPart) return null;
|
|
const prefixLength = Number(prefixPart);
|
|
if (!Number.isInteger(prefixLength) || prefixLength < 0) return null;
|
|
const parsed = parseIP(ipPart);
|
|
if (!parsed) return null;
|
|
let resolvedFamily = parsed.family;
|
|
let resolvedAddress = Buffer.from(parsed.bytes);
|
|
let resolvedPrefix = prefixLength;
|
|
if (parsed.family === 'ipv6' && parsed.mappedIpv4Bytes && prefixLength >= 96) {
|
|
resolvedFamily = 'ipv4';
|
|
resolvedAddress = Buffer.from(parsed.mappedIpv4Bytes);
|
|
resolvedPrefix = prefixLength - 96;
|
|
}
|
|
const maxPrefix = resolvedFamily === 'ipv4' ? 32 : 128;
|
|
if (resolvedPrefix > maxPrefix) return null;
|
|
const network = Buffer.from(resolvedAddress);
|
|
const fullBytes = Math.floor(resolvedPrefix / 8);
|
|
const remainingBits = resolvedPrefix % 8;
|
|
if (remainingBits > 0 && fullBytes < network.length) {
|
|
const mask = (0xff << (8 - remainingBits)) & 0xff;
|
|
network[fullBytes] &= mask;
|
|
for (let i = fullBytes + 1; i < network.length; i++) {
|
|
network[i] = 0;
|
|
}
|
|
} else {
|
|
for (let i = fullBytes; i < network.length; i++) {
|
|
network[i] = 0;
|
|
}
|
|
}
|
|
return {
|
|
family: resolvedFamily,
|
|
address: network,
|
|
prefixLength: resolvedPrefix,
|
|
};
|
|
}
|
|
|
|
export function parseIpBanEntry(value: string): IpBanEntry | null {
|
|
return parseRange(value) ?? parseSingle(value);
|
|
}
|
|
|
|
export function tryParseSingleIp(value: string): ParsedIpSingle | null {
|
|
const parsed = parseIpBanEntry(value);
|
|
return parsed?.type === 'single' ? parsed : null;
|
|
}
|