149 lines · 6.3 KB · TypeScript
RawDownload
export const normalizeHostname = (host: string): string =>  host    .toLowerCase()    .replace(/^\[|\]$/g, "")    .replace(/\.+$/u, ""); const parseIpv4Address = (host: string): readonly number[] | null => {  const parts = normalizeHostname(host).split(".").map(Number);  return parts.length === 4 &&    parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255)    ? parts    : null;}; const parseIpv4MappedIpv6Address = (host: string): readonly number[] | null => {  const normalized = normalizeHostname(host);  if (!normalized.startsWith("::ffff:")) return null;  const suffix = normalized.slice("::ffff:".length);  const dotted = parseIpv4Address(suffix);  if (dotted) return dotted;  const hextets = suffix.split(":");  if (hextets.length !== 2 || hextets.some((part) => !/^[\da-f]{1,4}$/u.test(part))) return null;  const high = Number.parseInt(hextets[0]!, 16);  const low = Number.parseInt(hextets[1]!, 16);  return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];}; const parseIpv6Address = (host: string): readonly number[] | null => {  const normalized = normalizeHostname(host);  if (!normalized.includes(":")) return null;  const halves = normalized.split("::");  if (halves.length > 2) return null;  const head = halves[0] ? halves[0].split(":") : [];  const tail = halves[1] ? halves[1].split(":") : [];  if ([...head, ...tail].some((part) => !/^[\da-f]{1,4}$/u.test(part))) return null;  const missing = 8 - head.length - tail.length;  if ((halves.length === 1 && missing !== 0) || (halves.length === 2 && missing < 1)) return null;  return [...head, ...Array.from({ length: missing }, () => "0"), ...tail].map((part) =>    Number.parseInt(part, 16),  );}; const ipv6PrefixMatches = (  address: readonly number[],  prefix: readonly number[],  prefixLength: number,): boolean => {  const fullHextets = Math.floor(prefixLength / 16);  if (address.slice(0, fullHextets).some((part, index) => part !== prefix[index])) return false;  const remainingBits = prefixLength % 16;  if (remainingBits === 0) return true;  const mask = (0xffff << (16 - remainingBits)) & 0xffff;  return (address[fullHextets]! & mask) === (prefix[fullHextets]! & mask);}; const isPrivateIpv4Address = (parts: readonly number[]): boolean =>  parts[0] === 0 ||  parts[0] === 10 ||  parts[0] === 127 ||  (parts[0] === 100 && parts[1]! >= 64 && parts[1]! <= 127) ||  (parts[0] === 172 && parts[1]! >= 16 && parts[1]! <= 31) ||  (parts[0] === 192 && parts[1] === 168) ||  (parts[0] === 169 && parts[1] === 254) ||  (parts[0] === 198 && parts[1]! >= 18 && parts[1]! <= 19); const isSpecialPurposeIpv4Address = (parts: readonly number[]): boolean =>  isPrivateIpv4Address(parts) ||  parts[0]! >= 224 ||  // Deliberately suppress the whole protocol-assignment block. IANA marks  // .9 and .10 globally reachable, but privacy-safe false negatives are  // preferable to disclosing another special-purpose address by mistake.  (parts[0] === 192 && parts[1] === 0 && parts[2] === 0) ||  (parts[0] === 192 && parts[1] === 0 && parts[2] === 2) ||  (parts[0] === 192 && parts[1] === 88 && parts[2] === 99) ||  (parts[0] === 198 && parts[1] === 51 && parts[2] === 100) ||  (parts[0] === 203 && parts[1] === 0 && parts[2] === 113); export const isLocalLoopbackHost = (host: string): boolean => {  const normalized = normalizeHostname(host);  if (normalized === "localhost" || normalized === "::1") return true;  return parseIpv4Address(normalized)?.[0] === 127;}; export const isPrivateNetworkHost = (host: string): boolean => {  const normalized = normalizeHostname(host);  if (    normalized === "::" ||    isLocalLoopbackHost(normalized) ||    normalized.endsWith(".localhost") ||    normalized.endsWith(".local") ||    normalized === "home.arpa" ||    normalized.endsWith(".home.arpa") ||    (!normalized.includes(".") && !normalized.includes(":"))  ) {    return true;  }  if (normalized.endsWith(".ts.net")) return true;  const parts = parseIpv4Address(normalized) ?? parseIpv4MappedIpv6Address(normalized);  if (parts) return isPrivateIpv4Address(parts);  const firstIpv6Token = normalized.split(":", 1)[0] ?? "";  if (!normalized.includes(":") || !/^[\da-f]{1,4}$/u.test(firstIpv6Token)) return false;  const firstIpv6Hextet = Number.parseInt(firstIpv6Token, 16);  return (    Number.isInteger(firstIpv6Hextet) &&    ((firstIpv6Hextet & 0xfe00) === 0xfc00 || (firstIpv6Hextet & 0xffc0) === 0xfe80)  );}; /** Whether a hostname is eligible to be disclosed to a public favicon provider. */export const isPublicFaviconHost = (host: string): boolean => {  // A single trailing dot is a valid absolute DNS name. Repeated trailing  // dots are malformed and can conceal legacy numeric forms such as 127.1.  if (host.endsWith("..")) return false;  const normalized = normalizeHostname(host);  if (isPrivateNetworkHost(normalized)) return false;  if (    [".alt", ".example", ".internal", ".invalid", ".onion", ".test"].some(      (suffix) => normalized === suffix.slice(1) || normalized.endsWith(suffix),    )  ) {    return false;  }  const ipv4 = parseIpv4Address(normalized) ?? parseIpv4MappedIpv6Address(normalized);  if (ipv4) return !isSpecialPurposeIpv4Address(ipv4);  if (!normalized.includes(":")) return true;  const ipv6 = parseIpv6Address(normalized);  if (!ipv6) return false;  if (ipv6PrefixMatches(ipv6, [0x0064, 0xff9b, 0, 0, 0, 0, 0, 0], 96)) {    const embeddedIpv4 = [ipv6[6]! >>> 8, ipv6[6]! & 0xff, ipv6[7]! >>> 8, ipv6[7]! & 0xff];    return !isSpecialPurposeIpv4Address(embeddedIpv4);  }  const first = ipv6[0]!;  if ((first & 0xe000) !== 0x2000) return false;  if (ipv6PrefixMatches(ipv6, [0x2001, 0, 0, 0, 0, 0, 0, 0], 23)) {    const publicProtocolAssignment =      (ipv6[1] === 1 &&        ipv6.slice(2, 7).every((part) => part === 0) &&        [1, 2, 3].includes(ipv6[7]!)) ||      ipv6PrefixMatches(ipv6, [0x2001, 3, 0, 0, 0, 0, 0, 0], 32) ||      ipv6PrefixMatches(ipv6, [0x2001, 4, 0x0112, 0, 0, 0, 0, 0], 48) ||      ipv6PrefixMatches(ipv6, [0x2001, 0x20, 0, 0, 0, 0, 0, 0], 28) ||      ipv6PrefixMatches(ipv6, [0x2001, 0x30, 0, 0, 0, 0, 0, 0], 28);    return publicProtocolAssignment;  }  if (ipv6PrefixMatches(ipv6, [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0], 32)) return false;  if (ipv6PrefixMatches(ipv6, [0x2002, 0, 0, 0, 0, 0, 0, 0], 16)) return false;  if (first === 0x3fff && (ipv6[1]! & 0xf000) === 0) return false;  return true;};