87 lines
2.6 KiB
TypeScript
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}`;
|
|
}
|