73 lines
2.9 KiB
TypeScript
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;
|
|
}
|