Release Network Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user