UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

273 lines (270 loc) 11.1 kB
import { lookup } from "node:dns"; import { lookup as lookup$1 } from "node:dns/promises"; import { Agent } from "undici"; //#region src/infra/net/hostname.ts function normalizeHostname(hostname) { const normalized = hostname.trim().toLowerCase().replace(/\.$/, ""); if (normalized.startsWith("[") && normalized.endsWith("]")) return normalized.slice(1, -1); return normalized; } //#endregion //#region src/infra/net/ssrf.ts var SsrFBlockedError = class extends Error { constructor(message) { super(message); this.name = "SsrFBlockedError"; } }; const BLOCKED_HOSTNAMES = new Set([ "localhost", "localhost.localdomain", "metadata.google.internal" ]); function normalizeHostnameSet(values) { if (!values || values.length === 0) return /* @__PURE__ */ new Set(); return new Set(values.map((value) => normalizeHostname(value)).filter(Boolean)); } function normalizeHostnameAllowlist(values) { if (!values || values.length === 0) return []; return Array.from(new Set(values.map((value) => normalizeHostname(value)).filter((value) => value !== "*" && value !== "*." && value.length > 0))); } function isHostnameAllowedByPattern(hostname, pattern) { if (pattern.startsWith("*.")) { const suffix = pattern.slice(2); if (!suffix || hostname === suffix) return false; return hostname.endsWith(`.${suffix}`); } return hostname === pattern; } function matchesHostnameAllowlist(hostname, allowlist) { if (allowlist.length === 0) return true; return allowlist.some((pattern) => isHostnameAllowedByPattern(hostname, pattern)); } function parseStrictIpv4Octet(part) { if (!/^[0-9]+$/.test(part)) return null; const value = Number.parseInt(part, 10); if (Number.isNaN(value) || value < 0 || value > 255) return null; if (part !== String(value)) return null; return value; } function parseIpv4(address) { const parts = address.split("."); if (parts.length !== 4) return null; for (const part of parts) if (parseStrictIpv4Octet(part) === null) return null; return parts.map((part) => Number.parseInt(part, 10)); } function classifyIpv4Part(part) { if (/^0x[0-9a-f]+$/i.test(part)) return "hex"; if (/^0x/i.test(part)) return "invalid-hex"; if (/^[0-9]+$/.test(part)) return "decimal"; return "non-numeric"; } function isUnsupportedLegacyIpv4Literal(address) { const parts = address.split("."); if (parts.length === 0 || parts.length > 4) return false; if (parts.some((part) => part.length === 0)) return true; const partKinds = parts.map(classifyIpv4Part); if (partKinds.some((kind) => kind === "non-numeric")) return false; if (partKinds.some((kind) => kind === "invalid-hex")) return true; if (parts.length !== 4) return true; for (const part of parts) { if (/^0x/i.test(part)) return true; const value = Number.parseInt(part, 10); if (Number.isNaN(value) || value > 255 || part !== String(value)) return true; } return false; } function stripIpv6ZoneId(address) { const index = address.indexOf("%"); return index >= 0 ? address.slice(0, index) : address; } function parseIpv6Hextets(address) { let input = stripIpv6ZoneId(address.trim().toLowerCase()); if (!input) return null; if (input.includes(".")) { const lastColon = input.lastIndexOf(":"); if (lastColon < 0) return null; const ipv4 = parseIpv4(input.slice(lastColon + 1)); if (!ipv4) return null; const high = (ipv4[0] << 8) + ipv4[1]; const low = (ipv4[2] << 8) + ipv4[3]; input = `${input.slice(0, lastColon)}:${high.toString(16)}:${low.toString(16)}`; } const doubleColonParts = input.split("::"); if (doubleColonParts.length > 2) return null; const headParts = doubleColonParts[0]?.length > 0 ? doubleColonParts[0].split(":").filter(Boolean) : []; const tailParts = doubleColonParts.length === 2 && doubleColonParts[1]?.length > 0 ? doubleColonParts[1].split(":").filter(Boolean) : []; const missingParts = 8 - headParts.length - tailParts.length; if (missingParts < 0) return null; const fullParts = doubleColonParts.length === 1 ? input.split(":") : [ ...headParts, ...Array.from({ length: missingParts }, () => "0"), ...tailParts ]; if (fullParts.length !== 8) return null; const hextets = []; for (const part of fullParts) { if (!part) return null; const value = Number.parseInt(part, 16); if (Number.isNaN(value) || value < 0 || value > 65535) return null; hextets.push(value); } return hextets; } function decodeIpv4FromHextets(high, low) { return [ high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255 ]; } const EMBEDDED_IPV4_RULES = [ { matches: (hextets) => hextets[0] === 0 && hextets[1] === 0 && hextets[2] === 0 && hextets[3] === 0 && hextets[4] === 0 && (hextets[5] === 65535 || hextets[5] === 0), extract: (hextets) => [hextets[6], hextets[7]] }, { matches: (hextets) => hextets[0] === 100 && hextets[1] === 65435 && hextets[2] === 0 && hextets[3] === 0 && hextets[4] === 0 && hextets[5] === 0, extract: (hextets) => [hextets[6], hextets[7]] }, { matches: (hextets) => hextets[0] === 100 && hextets[1] === 65435 && hextets[2] === 1 && hextets[3] === 0 && hextets[4] === 0 && hextets[5] === 0, extract: (hextets) => [hextets[6], hextets[7]] }, { matches: (hextets) => hextets[0] === 8194, extract: (hextets) => [hextets[1], hextets[2]] }, { matches: (hextets) => hextets[0] === 8193 && hextets[1] === 0, extract: (hextets) => [hextets[6] ^ 65535, hextets[7] ^ 65535] }, { matches: (hextets) => (hextets[4] & 64767) === 0 && hextets[5] === 24318, extract: (hextets) => [hextets[6], hextets[7]] } ]; function extractIpv4FromEmbeddedIpv6(hextets) { for (const rule of EMBEDDED_IPV4_RULES) { if (!rule.matches(hextets)) continue; const [high, low] = rule.extract(hextets); return decodeIpv4FromHextets(high, low); } return null; } function isPrivateIpv4(parts) { const [octet1, octet2] = parts; if (octet1 === 0) return true; if (octet1 === 10) return true; if (octet1 === 127) return true; if (octet1 === 169 && octet2 === 254) return true; if (octet1 === 172 && octet2 >= 16 && octet2 <= 31) return true; if (octet1 === 192 && octet2 === 168) return true; if (octet1 === 100 && octet2 >= 64 && octet2 <= 127) return true; return false; } function isPrivateIpAddress(address) { let normalized = address.trim().toLowerCase(); if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1); if (!normalized) return false; if (normalized.includes(":")) { const hextets = parseIpv6Hextets(normalized); if (!hextets) return true; const isUnspecified = hextets[0] === 0 && hextets[1] === 0 && hextets[2] === 0 && hextets[3] === 0 && hextets[4] === 0 && hextets[5] === 0 && hextets[6] === 0 && hextets[7] === 0; const isLoopback = hextets[0] === 0 && hextets[1] === 0 && hextets[2] === 0 && hextets[3] === 0 && hextets[4] === 0 && hextets[5] === 0 && hextets[6] === 0 && hextets[7] === 1; if (isUnspecified || isLoopback) return true; const embeddedIpv4 = extractIpv4FromEmbeddedIpv6(hextets); if (embeddedIpv4) return isPrivateIpv4(embeddedIpv4); const first = hextets[0]; if ((first & 65472) === 65152) return true; if ((first & 65472) === 65216) return true; if ((first & 65024) === 64512) return true; return false; } const ipv4 = parseIpv4(normalized); if (ipv4) return isPrivateIpv4(ipv4); if (isUnsupportedLegacyIpv4Literal(normalized)) return true; return false; } function isBlockedHostnameNormalized(normalized) { if (BLOCKED_HOSTNAMES.has(normalized)) return true; return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal"); } function isBlockedHostnameOrIp(hostname) { const normalized = normalizeHostname(hostname); if (!normalized) return false; return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized); } function createPinnedLookup(params) { const normalizedHost = normalizeHostname(params.hostname); const fallback = params.fallback ?? lookup; const fallbackLookup = fallback; const fallbackWithOptions = fallback; const records = params.addresses.map((address) => ({ address, family: address.includes(":") ? 6 : 4 })); let index = 0; return ((host, options, callback) => { const cb = typeof options === "function" ? options : callback; if (!cb) return; const normalized = normalizeHostname(host); if (!normalized || normalized !== normalizedHost) { if (typeof options === "function" || options === void 0) return fallbackLookup(host, cb); return fallbackWithOptions(host, options, cb); } const opts = typeof options === "object" && options !== null ? options : {}; const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0; const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records; const usable = candidates.length > 0 ? candidates : records; if (opts.all) { cb(null, usable); return; } const chosen = usable[index % usable.length]; index += 1; cb(null, chosen.address, chosen.family); }); } async function resolvePinnedHostnameWithPolicy(hostname, params = {}) { const normalized = normalizeHostname(hostname); if (!normalized) throw new Error("Invalid hostname"); const allowPrivateNetwork = Boolean(params.policy?.allowPrivateNetwork); const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames); const hostnameAllowlist = normalizeHostnameAllowlist(params.policy?.hostnameAllowlist); const isExplicitAllowed = allowedHostnames.has(normalized); if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) throw new SsrFBlockedError(`Blocked hostname (not in allowlist): ${hostname}`); if (!allowPrivateNetwork && !isExplicitAllowed && isBlockedHostnameOrIp(normalized)) throw new SsrFBlockedError("Blocked hostname or private/internal IP address"); const results = await (params.lookupFn ?? lookup$1)(normalized, { all: true }); if (results.length === 0) throw new Error(`Unable to resolve hostname: ${hostname}`); if (!allowPrivateNetwork && !isExplicitAllowed) { for (const entry of results) if (isPrivateIpAddress(entry.address)) throw new SsrFBlockedError("Blocked: resolves to private/internal IP address"); } const addresses = Array.from(new Set(results.map((entry) => entry.address))); if (addresses.length === 0) throw new Error(`Unable to resolve hostname: ${hostname}`); return { hostname: normalized, addresses, lookup: createPinnedLookup({ hostname: normalized, addresses }) }; } function createPinnedDispatcher(pinned) { return new Agent({ connect: { lookup: pinned.lookup } }); } async function closeDispatcher(dispatcher) { if (!dispatcher) return; const candidate = dispatcher; try { if (typeof candidate.close === "function") { await candidate.close(); return; } if (typeof candidate.destroy === "function") candidate.destroy(); } catch {} } //#endregion export { resolvePinnedHostnameWithPolicy as a, isBlockedHostnameOrIp as i, closeDispatcher as n, normalizeHostname as o, createPinnedDispatcher as r, SsrFBlockedError as t };