Files

73 lines
2.9 KiB
TypeScript

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<PublicResolution> {
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;
}