Files
network-tools/src/network/dns.ts
T
2026-09-01 02:44:20 +02:00

87 lines
2.6 KiB
TypeScript

import { parseIpv4, parseIpv6 } from "@add-ideas/toolbox-helpers";
export type DnsRecordType =
"A" | "AAAA" | "CAA" | "CNAME" | "MX" | "SRV" | "TXT";
export interface DnsRecordInput {
owner: string;
ttl: number;
type: DnsRecordType;
value: string;
priority?: number;
weight?: number;
port?: number;
flag?: number;
tag?: "issue" | "issuewild" | "iodef";
}
const LABEL =
/^(?:[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?)(?:\.(?:[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?))*\.?$/u;
function integer(
value: number | undefined,
name: string,
maximum = 65_535,
): number {
if (!Number.isInteger(value) || value! < 0 || value! > maximum)
throw new Error(`${name} must be an integer from 0 to ${maximum}.`);
return value!;
}
function domain(value: string, name: string): string {
const candidate = value.trim();
if (candidate === "@") return candidate;
if (!candidate || candidate.length > 253 || !LABEL.test(candidate))
throw new Error(`${name} must be a valid ASCII DNS name.`);
return candidate;
}
function quoteTxt(value: string): string {
const bytes = new TextEncoder().encode(value);
if (bytes.length > 255)
throw new Error("A TXT character-string is limited to 255 UTF-8 bytes.");
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
}
export function buildDnsRecord(input: DnsRecordInput): string {
const owner = domain(input.owner, "Owner");
const ttl = integer(input.ttl, "TTL", 2_147_483_647);
const value = input.value.trim();
if (!value) throw new Error("Record value is required.");
let data: string;
switch (input.type) {
case "A":
try {
data = parseIpv4(value).canonical;
} catch {
throw new Error("A records require a dotted-decimal IPv4 address.");
}
break;
case "AAAA":
try {
data = parseIpv6(value).canonical;
} catch {
throw new Error("AAAA records require an IPv6 address.");
}
break;
case "CNAME":
data = domain(value, "Target");
break;
case "MX":
data = `${integer(input.priority, "Priority")} ${domain(value, "Mail exchanger")}`;
break;
case "SRV":
data = `${integer(input.priority, "Priority")} ${integer(input.weight, "Weight")} ${integer(input.port, "Port")} ${domain(value, "Target")}`;
break;
case "CAA": {
const tag = input.tag ?? "issue";
data = `${integer(input.flag ?? 0, "Flag", 255)} ${tag} ${quoteTxt(value)}`;
break;
}
case "TXT":
data = quoteTxt(value);
break;
}
return `${owner} ${ttl} IN ${input.type} ${data}`;
}