mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-10-26 08:26:22 -04:00
fix: read WHOIS nameservers and their glue across ccTLD layouts
Nameservers were missing for .pl, .be, .it ("nameservers"), .hk ("Name Servers
Information"), .kr ("Host Name"), and .tm past "NS 4"; .pl kept only its first
host because each continuation line's IPv6 colon made it a new key. Glue was
read only as bare space-separated addresses, so it was lost when written in
parentheses (.cz, .cl, .mk, .ve, .si), brackets (.lu, .lt, .pl), or with commas
(.ru's IPv4, .mx's IPv6). .dk listed the domain itself (its "DNS:" line) as a
nameserver.
Add the keys, take glue from any of those shapes, treat an indented line whose
colons aren't key separators as a continuation, and skip entries that aren't
hostnames or name the queried domain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+106
-35
No files matched your search
@@ -61,6 +61,12 @@ export function parseKeyValueLines(text: string): Record<string, string[]> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// An indented line whose colons aren't key separators continues the last key, e.g. .pl's
|
||||
// " eomer.nask.net.pl. [193.59.201.24][2001:a10:1:ffff::2:c918]"
|
||||
if (lastKey && indent > 0 && !/:(\s|$)/.test(line)) {
|
||||
add(lastKey, line.trim());
|
||||
continue;
|
||||
}
|
||||
// Colon form: Key: value
|
||||
const idx = line.indexOf(":");
|
||||
if (idx !== -1) {
|
||||
|
||||
@@ -525,3 +525,60 @@ test("WHOIS statuses come from the domain's block, whole and split on commas", (
|
||||
status("Domain: x.be\nStatus: NOT AVAILABLE\n\nFlags:\n\tclientTransferProhibited\n"),
|
||||
).toEqual(["NOT AVAILABLE", "clientTransferProhibited"]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"nserver: d.ns.nic.cz (193.29.206.1, 2001:678:1::1)",
|
||||
{ ipv4: ["193.29.206.1"], ipv6: ["2001:678:1::1"] },
|
||||
], // .cz
|
||||
[
|
||||
"nserver: k.dns.lu [194.0.1.13,2001:678:4::d]",
|
||||
{ ipv4: ["194.0.1.13"], ipv6: ["2001:678:4::d"] },
|
||||
], // .lu
|
||||
[
|
||||
"nserver: ns5.nic.ru. 31.177.67.100, 2a02:2090:e800::100",
|
||||
{ ipv4: ["31.177.67.100"], ipv6: ["2a02:2090:e800::100"] },
|
||||
], // .ru
|
||||
["DNS: odisej.telekom.rs - 195.178.32.2", { ipv4: ["195.178.32.2"] }], // .rs
|
||||
[
|
||||
"Name Server: b.dns.pt | IPv4: 194.0.25.23 and IPv6: 2001:678:20::23",
|
||||
{ ipv4: ["194.0.25.23"], ipv6: ["2001:678:20::23"] },
|
||||
], // .pt
|
||||
])("WHOIS nameserver glue in %j", (line, glue) => {
|
||||
const rec = normalizeWhois(
|
||||
"example.test",
|
||||
"test",
|
||||
`Domain Name: example.test\n${line}\n`,
|
||||
undefined,
|
||||
);
|
||||
expect(rec.nameservers).toHaveLength(1);
|
||||
expect(rec.nameservers?.[0]).toMatchObject(glue);
|
||||
});
|
||||
|
||||
test("WHOIS nameserver keys and continuation lines across ccTLD layouts", () => {
|
||||
const hosts = (domain: string, text: string) =>
|
||||
normalizeWhois(domain, "test", text, undefined).nameservers?.map((n) => n.host);
|
||||
// .pl: continuation lines carrying IPv6 glue
|
||||
expect(
|
||||
hosts(
|
||||
"dns.pl",
|
||||
"nameservers: bilbo.nask.org.pl. [195.187.245.51]\n eomer.nask.net.pl. [193.59.201.24][2001:a10:1:ffff::2:c918]\ncreated: 1998.01.26 12:00:00\n",
|
||||
),
|
||||
).toEqual(["bilbo.nask.org.pl", "eomer.nask.net.pl"]);
|
||||
// .kr "Host Name", .tm "NS 7"
|
||||
expect(
|
||||
hosts("kisa.or.kr", "Primary Name Server\n Host Name : center.kisa.or.kr\n"),
|
||||
).toEqual(["center.kisa.or.kr"]);
|
||||
expect(hosts("nic.tm", "NS 1 : dns1.nic.tm\nNS 7 : dns6.nic.tm\n")).toEqual([
|
||||
"dns1.nic.tm",
|
||||
"dns6.nic.tm",
|
||||
]);
|
||||
// .dk names the domain itself on a "DNS:" line; .lv writes "-" for none
|
||||
expect(
|
||||
hosts(
|
||||
"punktum.dk",
|
||||
"Domain: punktum.dk\nDNS: punktum.dk\n\nHostname: auth01.ns.dk-hostmaster.dk\n",
|
||||
),
|
||||
).toEqual(["auth01.ns.dk-hostmaster.dk"]);
|
||||
expect(hosts("nic.lv", "Domain: nic.lv\nNserver: -\n")).toBeUndefined();
|
||||
});
|
||||
+43
-35
@@ -198,43 +198,14 @@ export function normalizeWhois(
|
||||
|
||||
// Nameservers: also appear as "nserver" on some ccTLDs (.de, .ru) and as "name server"
|
||||
const nsLines: string[] = [
|
||||
...(map["name server"] || []),
|
||||
...(map.nameserver || []),
|
||||
...(map["name servers"] || []),
|
||||
...(map.nserver || []),
|
||||
...(map["name server information"] || []),
|
||||
...(map.dns || []),
|
||||
...(map.hostname || []),
|
||||
...(map["domain nameservers"] || []),
|
||||
...(map["domain servers in listed order"] || []), // .ly
|
||||
...(map["domain servers"] || []), // .tr
|
||||
...(map["name servers dns"] || []), // .mx
|
||||
...(map["ns 1"] || []),
|
||||
...(map["ns 2"] || []),
|
||||
...(map["ns 3"] || []),
|
||||
...(map["ns 4"] || []),
|
||||
...NAMESERVER_KEYS.flatMap((k) => map[k] ?? []),
|
||||
// "NS 1" ... "NS 8" (.tm)
|
||||
...Object.keys(map)
|
||||
.filter((k) => /^ns \d+$/.test(k))
|
||||
.flatMap((k) => map[k] ?? []),
|
||||
];
|
||||
const nameservers = mergeNameservers(
|
||||
nsLines
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
// Common formats: "ns1.example.com" or "ns1.example.com 192.0.2.1" or "ns1.example.com 2001:db8::1"
|
||||
const parts = line.split(/\s+/);
|
||||
const host = parts.shift()?.toLowerCase() || "";
|
||||
const ipv4: string[] = [];
|
||||
const ipv6: string[] = [];
|
||||
for (const p of parts) {
|
||||
if (/^\d+\.\d+\.\d+\.\d+$/.test(p)) ipv4.push(p);
|
||||
else if (/^[0-9a-f:]+$/i.test(p)) ipv6.push(p);
|
||||
}
|
||||
if (!host) return undefined;
|
||||
const ns: Nameserver = { host };
|
||||
if (ipv4.length) ns.ipv4 = ipv4;
|
||||
if (ipv6.length) ns.ipv6 = ipv6;
|
||||
return ns;
|
||||
})
|
||||
.filter((x): x is Nameserver => !!x),
|
||||
nsLines.map((line) => parseNameserverLine(line, domain)).filter((x): x is Nameserver => !!x),
|
||||
);
|
||||
|
||||
// Contacts: best-effort parse common keys
|
||||
@@ -290,6 +261,43 @@ export function normalizeWhois(
|
||||
return record;
|
||||
}
|
||||
|
||||
const NAMESERVER_KEYS = [
|
||||
"name server",
|
||||
"nameserver",
|
||||
"name servers",
|
||||
"nameservers", // .pl, .be, .it
|
||||
"nserver",
|
||||
"name server information",
|
||||
"name servers information", // .hk
|
||||
"dns",
|
||||
"hostname",
|
||||
"host name", // .kr
|
||||
"domain nameservers",
|
||||
"domain servers in listed order", // .ly
|
||||
"domain servers", // .tr
|
||||
"name servers dns", // .mx
|
||||
];
|
||||
|
||||
/**
|
||||
* One nameserver line: the host, then glue in whatever shape the registry uses ("192.0.2.1
|
||||
* 2001:db8::1", "(192.0.2.1)", "[192.0.2.1,2001:db8::1]" (.lu), "- 192.0.2.1" (.rs),
|
||||
* "| IPv4: 192.0.2.1 and IPv6: 2001:db8::1" (.pt)). Undefined when the line doesn't start
|
||||
* with a hostname ("-" on .lv) or names the queried domain itself (.dk's "DNS:" line).
|
||||
*/
|
||||
function parseNameserverLine(line: string, domain: string): Nameserver | undefined {
|
||||
const [first = "", ...rest] = line.split(/[\s,;()[\]|]+/).filter(Boolean);
|
||||
const host = first.toLowerCase().replace(/\.$/, "");
|
||||
if (!/^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+$/u.test(host) || host === domain.toLowerCase()) {
|
||||
return undefined;
|
||||
}
|
||||
const ipv4 = rest.filter((t) => /^\d{1,3}(?:\.\d{1,3}){3}$/.test(t));
|
||||
const ipv6 = rest.filter((t) => /^[0-9a-f]*(?::[0-9a-f]*){2,7}$/i.test(t));
|
||||
const ns: Nameserver = { host };
|
||||
if (ipv4.length) ns.ipv4 = ipv4;
|
||||
if (ipv6.length) ns.ipv6 = ipv6;
|
||||
return ns;
|
||||
}
|
||||
|
||||
// Status keys by preference: .fr lists EPP codes as "eppstatus" beside a plain "status: ACTIVE"
|
||||
const STATUS_KEYS = ["eppstatus", "domain status", "status", "registration status"];
|
||||
|
||||
|
||||
Reference in new issue
Block a user