import ipaddr from "ipaddr.js"; import { domainToASCII } from "node:url"; import { DNS_TIMEOUT_MS, resolveDnsFamily, type DnsFamilyResult } from "./dns"; import { createDeadline } from "./deadline"; const HOST_RE = /^(?=.{1,253}$)(?!-)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; export { DNS_TIMEOUT_MS } from "./dns"; export class InspectionInputError extends Error { readonly kind = "input" as const; } export class InspectionPolicyError extends Error { readonly kind = "policy" as const; } export class InspectionResolutionError extends Error { readonly kind = "resolution" as const; } export function normalizeHost(input: string) { if (typeof input !== "string") throw new InspectionInputError("Enter a public DNS hostname, not a URL or IP address."); const trimmed = input.trim(); if (!trimmed || trimmed.length > 253) throw new InspectionInputError("Enter a public DNS hostname, not a URL or IP address."); const host = domainToASCII(trimmed.toLowerCase().replace(/\.$/, "")); if (!HOST_RE.test(host) || ipaddr.isValid(host)) { throw new InspectionInputError("Enter a public DNS hostname, not a URL or IP address."); } return host; } export function assertPublicAddress(address: string) { try { const range = ipaddr.parse(address).range(); if (range !== "unicast") throw new InspectionPolicyError("Target resolved to non-public address space (" + range + ")."); } catch (error) { if (error instanceof InspectionPolicyError) throw error; throw new InspectionPolicyError("Target resolved to an invalid address."); } } export function validateResolvedAddresses(addresses: string[]) { if (!addresses.length) throw new InspectionResolutionError("Hostname did not resolve."); const normalized = [...new Set(addresses)]; normalized.forEach(assertPublicAddress); return normalized; } export interface PublicResolution { addresses: string[]; families: { ipv4: DnsFamilyResult; ipv6: DnsFamilyResult }; _meta: { status: "ok" | "partial"; partial: boolean; errors: Array<{ probe: string; message: string }> }; } export async function resolvePublicHostDetailed(host: string, deadline = createDeadline(DNS_TIMEOUT_MS)): Promise { const normalized = normalizeHost(host); const [ipv4, ipv6] = await Promise.all([ resolveDnsFamily(normalized, 4, deadline), resolveDnsFamily(normalized, 6, deadline), ]); const addresses = validateResolvedAddresses([...ipv4.addresses, ...ipv6.addresses]); const errors = [ipv4.error, ipv6.error].filter((error): error is { probe: string; message: string } => Boolean(error)); return { addresses, families: { ipv4, ipv6 }, _meta: { status: errors.length ? "partial" : "ok", partial: errors.length > 0, errors }, }; } export async function resolvePublicHost(host: string) { return (await resolvePublicHostDetailed(host)).addresses; }