@slicedearth/whoisleuth-cli 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/arguments.mjs +21 -5
- package/cli/artifact-validation/case-response.mjs +771 -0
- package/cli/artifact-validation/investigation-capsule.mjs +426 -0
- package/cli/artifact-validation/lookup-evidence.mjs +718 -0
- package/cli/artifact-validation/signed-review.mjs +598 -0
- package/cli/artifact-validation/structure-primitives.mjs +105 -0
- package/cli/artifact-verify.mjs +1 -1
- package/cli/assurance-command-runner.mjs +324 -0
- package/cli/command-reference.mjs +1175 -843
- package/cli/completion.mjs +56 -9
- package/cli/discriminated-command-handlers.mjs +11 -0
- package/cli/doctor.mjs +2 -2
- package/cli/evidence-command-runner.mjs +41 -31
- package/cli/export-evidence.mjs +1 -1
- package/cli/fail-policy.mjs +33 -8
- package/cli/formatters/terminal-command-formats.mjs +437 -0
- package/cli/formatters/terminal-lookup.mjs +758 -0
- package/cli/formatters/terminal-metadata.mjs +58 -0
- package/cli/formatters/terminal-shared.mjs +83 -0
- package/cli/formatters/terminal.mjs +5 -1232
- package/cli/history-command-runner.mjs +132 -0
- package/cli/inline-command-families.mjs +45 -0
- package/cli/inline-command-runner.mjs +55 -0
- package/cli/investigation-plan.mjs +9 -2
- package/cli/investigation-run.mjs +114 -9
- package/cli/lookup-brief.mjs +141 -23
- package/cli/lookup-browser-view.mjs +289 -0
- package/cli/lookup-browser.mjs +3 -288
- package/cli/mail-review.mjs +2 -6
- package/cli/offline-artifact-validation.mjs +19 -0
- package/cli/review-command-runner.mjs +269 -0
- package/cli/runner.mjs +15 -972
- package/cli/saved-lookup.mjs +19 -2
- package/cli/support-command-runner.mjs +229 -0
- package/cli/workflow-command-runner.mjs +129 -0
- package/docs/cli-reference.md +16 -9
- package/docs/cli.md +4 -2
- package/lib/availability.mjs +12 -9
- package/lib/bounded-contract-normalizers.mjs +2 -9
- package/lib/browser-library-profile.mjs +1 -2
- package/lib/client-behavior-profile.mjs +1 -2
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/domain-posture-analysis.mjs +10 -1
- package/lib/domain-posture.mjs +51 -23
- package/lib/evidence-export.mjs +46 -5
- package/lib/evidence-report-markdown.mjs +4 -0
- package/lib/evidence-report.mjs +50 -0
- package/lib/generated/cisa-kev-catalog.mjs +34 -4
- package/lib/generated/registrar-standing-catalogue.mjs +367 -0
- package/lib/json-record.mjs +12 -0
- package/lib/lookup-child-profile-contract.mjs +858 -0
- package/lib/lookup-contract-primitives.mjs +98 -0
- package/lib/lookup-diagnostics.mjs +96 -0
- package/lib/lookup-network-evidence-bounds.mjs +1 -0
- package/lib/lookup-response.mjs +367 -0
- package/lib/lookup.mjs +28 -443
- package/lib/page-role-profile.mjs +1 -3
- package/lib/registrar-standing-catalogue-contract.mjs +70 -0
- package/lib/registrar-standing-contract.mjs +300 -0
- package/lib/registrar-standing.mjs +269 -0
- package/lib/registry-insights.mjs +2 -8
- package/lib/risk-scoring.mjs +26 -10
- package/lib/sslbl-certificates.generated.mjs +138 -136
- package/lib/structured-data-identity.mjs +1 -3
- package/lib/technology-evidence-role.mjs +51 -0
- package/lib/threat-intelligence-runtime.mjs +3 -3
- package/lib/threat-intelligence-types.mjs +13 -0
- package/lib/threatfox-intelligence.mjs +1 -3
- package/lib/tls-intelligence.mjs +1 -2
- package/lib/urlhaus-intelligence.mjs +1 -3
- package/lib/urlscan-intelligence.mjs +1 -3
- package/lib/website-security-posture.mjs +1 -2
- package/lib/website-technology.mjs +72 -23
- package/lib/whois-common-formats.mjs +276 -0
- package/lib/whois-dialects.mjs +534 -0
- package/lib/whois-parser-support.mjs +305 -0
- package/lib/whois-parser.mjs +135 -0
- package/lib/whois.mjs +6 -1187
- package/package.json +2 -2
- package/packages/cases/case-evidence-model.mjs +9 -0
- package/packages/cases/case-migration-model.mjs +17 -11
- package/packages/cases/case-response-model.mjs +13 -9
- package/packages/cases/case-response-packet.mjs +297 -112
- package/packages/comparison/page-similarity.mjs +23 -0
- package/packages/contracts/case-portability.mjs +225 -61
- package/packages/contracts/cli-lookup.mjs +4 -3
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/investigation-portability.mjs +1 -1
- package/packages/contracts/risk-calibration.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +34 -6
- package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
- package/packages/investigation/lookup-claim-readiness.mjs +2 -2
- package/packages/workspace/website-snapshot-model.mjs +79 -6
- package/third-party-notices.txt +1 -1
- package/cli/artifact-structure.mjs +0 -2507
package/lib/whois.mjs
CHANGED
|
@@ -1,1187 +1,6 @@
|
|
|
1
|
-
// WHOIS:
|
|
2
|
-
// the
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { buildWhoisChain, buildWhoisChainUncached, } from "./whois-chain.mjs";
|
|
8
|
-
import { analyzeWhoisChainAuthority, hasNicKgRegistrationEvidence, } from "./whois-authority.mjs";
|
|
9
|
-
import { parseIndentedContactBlock, resolveFredContact, resolveIrnicContact, resolveIsnicRole, } from "./whois-contacts.mjs";
|
|
10
|
-
import { normalizeWhoisChain } from "./whois-normalization.mjs";
|
|
11
|
-
import { MAX_WHOIS_FIELD_LENGTH, boundedWhoisValue, parseBoundedWhoisSection, parseIndentedWhoisSubfield, parseIndentedWhoisValue, whoisFieldLimit, } from "./whois-values.mjs";
|
|
12
|
-
const MAX_WHOIS_NAMESERVERS = 200;
|
|
13
|
-
const MAX_WHOIS_STATUSES = 100;
|
|
14
|
-
const WHOIS_DOMAIN_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;
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// WHOIS response parsing (merges the referral chain into readable fields)
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
function assignBoundedWhoisMatch(text, fields, key, pattern, truncatedFields) {
|
|
19
|
-
if (fields[key])
|
|
20
|
-
return;
|
|
21
|
-
const match = text.match(pattern);
|
|
22
|
-
if (!match)
|
|
23
|
-
return;
|
|
24
|
-
const bounded = boundedWhoisValue(match[1], whoisFieldLimit(key));
|
|
25
|
-
if (!bounded.value)
|
|
26
|
-
return;
|
|
27
|
-
fields[key] = bounded.value;
|
|
28
|
-
if (bounded.truncated)
|
|
29
|
-
truncatedFields.add(key);
|
|
30
|
-
}
|
|
31
|
-
function addBoundedWhoisSetValue(set, rawValue, { maxEntries, maxLength, field, truncatedFields, }) {
|
|
32
|
-
const bounded = boundedWhoisValue(rawValue, maxLength);
|
|
33
|
-
if (!bounded.value || set.has(bounded.value))
|
|
34
|
-
return 'ignored';
|
|
35
|
-
if (set.size >= maxEntries) {
|
|
36
|
-
truncatedFields.add(field);
|
|
37
|
-
return 'capped';
|
|
38
|
-
}
|
|
39
|
-
set.add(bounded.value);
|
|
40
|
-
return 'added';
|
|
41
|
-
}
|
|
42
|
-
function collectBareWhoisNameservers(text, headerRe, nameservers, truncatedFields) {
|
|
43
|
-
const headerMatch = text.match(headerRe);
|
|
44
|
-
if (!headerMatch)
|
|
45
|
-
return;
|
|
46
|
-
const lines = text.slice((headerMatch.index ?? 0) + headerMatch[0].length)
|
|
47
|
-
.split('\n', MAX_WHOIS_NAMESERVERS + 3);
|
|
48
|
-
let found = 0;
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
const trimmed = line.trim();
|
|
51
|
-
if (!trimmed) {
|
|
52
|
-
if (found)
|
|
53
|
-
break;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const hostMatch = trimmed.match(/^([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\.?(?:\s|$)/);
|
|
57
|
-
if (!hostMatch)
|
|
58
|
-
break;
|
|
59
|
-
const hostname = hostMatch[1];
|
|
60
|
-
if (!hostname)
|
|
61
|
-
break;
|
|
62
|
-
const result = addBoundedWhoisSetValue(nameservers, hostname.replace(/\.$/, ''), {
|
|
63
|
-
maxEntries: MAX_WHOIS_NAMESERVERS,
|
|
64
|
-
maxLength: 253,
|
|
65
|
-
field: 'nameservers',
|
|
66
|
-
truncatedFields,
|
|
67
|
-
});
|
|
68
|
-
if (result === 'capped')
|
|
69
|
-
break;
|
|
70
|
-
if (result === 'added')
|
|
71
|
-
found += 1;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function normalizedWhoisContact(fields, prefix, role, truncatedFields) {
|
|
75
|
-
let address = fields[`${prefix}Address`] || [
|
|
76
|
-
fields[`${prefix}Street`], fields[`${prefix}City`], fields[`${prefix}State`],
|
|
77
|
-
fields[`${prefix}PostalCode`], fields[`${prefix}Country`],
|
|
78
|
-
].filter(Boolean).join(', ') || null;
|
|
79
|
-
if (address && !fields[`${prefix}Address`]) {
|
|
80
|
-
const bounded = boundedWhoisValue(address, 1000);
|
|
81
|
-
address = bounded.value;
|
|
82
|
-
if (address)
|
|
83
|
-
fields[`${prefix}Address`] = address;
|
|
84
|
-
if (bounded.truncated)
|
|
85
|
-
truncatedFields.add(`${prefix}Address`);
|
|
86
|
-
}
|
|
87
|
-
const handle = fields[`${prefix}Id`] || null;
|
|
88
|
-
const name = fields[`${prefix}Name`] || null;
|
|
89
|
-
const org = fields[`${prefix}Org`] || null;
|
|
90
|
-
const email = fields[`${prefix}Email`] || null;
|
|
91
|
-
const phone = fields[`${prefix}Phone`] || null;
|
|
92
|
-
if (![handle, name, org, email, phone, address].some(Boolean))
|
|
93
|
-
return null;
|
|
94
|
-
return {
|
|
95
|
-
handle,
|
|
96
|
-
roles: [role],
|
|
97
|
-
name,
|
|
98
|
-
names: name ? [name] : [],
|
|
99
|
-
org,
|
|
100
|
-
organizations: org ? [org] : [],
|
|
101
|
-
email,
|
|
102
|
-
emails: email ? [email] : [],
|
|
103
|
-
phone,
|
|
104
|
-
phones: phone ? [phone] : [],
|
|
105
|
-
address,
|
|
106
|
-
addresses: address ? [address] : [],
|
|
107
|
-
publicIds: handle ? [{ type: 'Registry contact ID', identifier: handle }] : [],
|
|
108
|
-
links: [],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function parseWhoisChain(chain) {
|
|
112
|
-
const source = normalizeWhoisChain(chain);
|
|
113
|
-
const fields = {};
|
|
114
|
-
const truncatedFields = new Set();
|
|
115
|
-
const expandedStreetFields = new Set();
|
|
116
|
-
// [ \t]* (not \s*) after each colon - same reasoning as extractReferral:
|
|
117
|
-
// several registries list a field with no value (e.g. "Registrant
|
|
118
|
-
// Organization: " followed directly by "Registrant Street: REDACTED"),
|
|
119
|
-
// and \s* would cross that blank line and capture the next field's own
|
|
120
|
-
// label as the value.
|
|
121
|
-
//
|
|
122
|
-
// Each field lists the standard ICANN thick-WHOIS label first, then
|
|
123
|
-
// common alternates seen on registries that predate/ignore that format
|
|
124
|
-
// (e.g. .it uses "Domain:"/"Created:"/"Expire Date:"). First match wins.
|
|
125
|
-
// This is a broad-coverage net, not a claim of full per-registry support -
|
|
126
|
-
// registries with entirely different conventions (e.g. .jp's bracketed
|
|
127
|
-
// dual-language format) still need their own dedicated handling.
|
|
128
|
-
// ^[ \t*]* (not ^\s*) - some registries prefix lines with "**" (e.g. .tr's
|
|
129
|
-
// "** Domain Name:"). [ \t.]* before the colon - some use dot-leaders
|
|
130
|
-
// (.tr's "Created on..........:") or extra spaces before the colon
|
|
131
|
-
// (.kr's "Domain Name :") instead of a colon right after
|
|
132
|
-
// the label.
|
|
133
|
-
const patterns = {
|
|
134
|
-
domainName: [
|
|
135
|
-
/^[ \t*]*Domain Name[ \t.]*:[ \t]*(.+)$/im,
|
|
136
|
-
/^[ \t*]*Domain[ \t.]*:[ \t]*(.+)$/im,
|
|
137
|
-
/^[ \t]*domain_name[ \t]*:[ \t]*(.+)$/im,
|
|
138
|
-
],
|
|
139
|
-
registryDomainId: [/^[ \t*]*Registry Domain ID[ \t.]*:[ \t]*(.+)$/im],
|
|
140
|
-
registrar: [
|
|
141
|
-
/^[ \t*]*Registrar[ \t.]*:[ \t]*(.+)$/im,
|
|
142
|
-
/^[ \t*]*Sponsoring Registrar[ \t.]*:[ \t]*(.+)$/im,
|
|
143
|
-
/^[ \t*]*Registrar Name[ \t.]*:[ \t]*(.+)$/im,
|
|
144
|
-
/^[ \t]*registrar_name[ \t]*:[ \t]*(.+)$/im,
|
|
145
|
-
],
|
|
146
|
-
registrarUrl: [/^[ \t*]*Registrar URL[ \t.]*:[ \t]*(.+)$/im],
|
|
147
|
-
registrarWhoisServer: [/^[ \t*]*Registrar WHOIS Server[ \t.]*:[ \t]*(.+)$/im],
|
|
148
|
-
registrarIanaId: [/^[ \t*]*Registrar IANA ID[ \t.]*:[ \t]*(.+)$/im],
|
|
149
|
-
reseller: [/^[ \t*]*Reseller[ \t.]*:[ \t]*(.+)$/im],
|
|
150
|
-
createdDate: [
|
|
151
|
-
/^[ \t*]*Creation Date[ \t.]*:[ \t]*(.+)$/im,
|
|
152
|
-
/^[ \t*]*Created(?: On)?[ \t.]*:[ \t]*(.+)$/im,
|
|
153
|
-
/^[ \t*]*Regist(?:ration|ered)(?: Time| Date| On)?[ \t.]*:[ \t]*(.+)$/im,
|
|
154
|
-
/^[ \t*]*Domain record activated[ \t.]*:[ \t]*(.+)$/im,
|
|
155
|
-
/^[ \t]*domain_dateregistered[ \t]*:[ \t]*(.+)$/im,
|
|
156
|
-
/^[ \t]*domain_datecreated[ \t]*:[ \t]*(.+)$/im,
|
|
157
|
-
],
|
|
158
|
-
expiryDate: [
|
|
159
|
-
/^[ \t*]*Registr(?:y|ar) Expiry Date[ \t.]*:[ \t]*(.+)$/im,
|
|
160
|
-
/^[ \t*]*Registrar Registration Expiration Date[ \t.]*:[ \t]*(.+)$/im,
|
|
161
|
-
/^[ \t*]*Expir(?:y|ation|e)s?(?: Date| On)?[ \t.]*:[ \t]*(.+)$/im,
|
|
162
|
-
/^[ \t*]*Valid Until[ \t.]*:[ \t]*(.+)$/im,
|
|
163
|
-
/^[ \t*]*Domain expires[ \t.]*:[ \t]*(.+)$/im,
|
|
164
|
-
/^[ \t*]*Renewal Date[ \t.]*:[ \t]*(.+)$/im,
|
|
165
|
-
/^[ \t*]*paid-till[ \t.]*:[ \t]*(.+)$/im,
|
|
166
|
-
/^[ \t]*domain_datebilleduntil[ \t]*:[ \t]*(.+)$/im,
|
|
167
|
-
],
|
|
168
|
-
updatedDate: [
|
|
169
|
-
/^[ \t*]*Updated Date[ \t.]*:[ \t]*(.+)$/im,
|
|
170
|
-
/^[ \t*]*Update Date[ \t.]*:[ \t]*(.+)$/im,
|
|
171
|
-
/^[ \t*]*Last Update(?:d)?(?: Date| On)?[ \t.]*:[ \t]*(.+)$/im,
|
|
172
|
-
/^[ \t*]*Last Modified[ \t.]*:[ \t]*(.+)$/im,
|
|
173
|
-
/^[ \t*]*Modified(?: Date)?[ \t.]*:[ \t]*(.+)$/im,
|
|
174
|
-
/^[ \t*]*Modification Date[ \t.]*:[ \t]*(.+)$/im,
|
|
175
|
-
/^[ \t*]*last-update[ \t.]*:[ \t]*(.+)$/im,
|
|
176
|
-
/^[ \t*]*Changed[ \t.]*:[ \t]*(.+)$/im,
|
|
177
|
-
/^[ \t*]*Domain record last updated[ \t.]*:[ \t]*(.+)$/im,
|
|
178
|
-
/^[ \t]*domain_datelastmodified[ \t]*:[ \t]*(.+)$/im,
|
|
179
|
-
],
|
|
180
|
-
abuseEmail: [/^[ \t*]*Registrar Abuse Contact Email[ \t.]*:[ \t]*(.+)$/im],
|
|
181
|
-
abusePhone: [/^[ \t*]*Registrar Abuse Contact Phone[ \t.]*:[ \t]*(.+)$/im],
|
|
182
|
-
dnssec: [
|
|
183
|
-
/^[ \t*]*DNSSEC[ \t.]*:[ \t]*(.+)$/im,
|
|
184
|
-
/^[ \t*]*Signed[ \t.]*:[ \t]*(.+)$/im,
|
|
185
|
-
/^[ \t]*domain_signed[ \t]*:[ \t]*(.+)$/im,
|
|
186
|
-
],
|
|
187
|
-
// auDA (.au) publishes the registrant's eligibility basis (e.g. an ABN/
|
|
188
|
-
// ACN for a company) alongside - and often instead of - a named contact,
|
|
189
|
-
// since .au domain eligibility is tied to a registrable Australian
|
|
190
|
-
// presence rather than an individual.
|
|
191
|
-
eligibilityType: [/^[ \t*]*Eligibility Type[ \t.]*:[ \t]*(.+)$/im],
|
|
192
|
-
eligibilityId: [/^[ \t*]*Eligibility ID[ \t.]*:[ \t]*(.+)$/im],
|
|
193
|
-
// Standard ICANN thick-WHOIS registrant/admin fields - present verbatim
|
|
194
|
-
// on registries that don't redact contact data, and also what the .gt
|
|
195
|
-
// web-lookup fallback below is formatted to produce. Several registries
|
|
196
|
-
// (e.g. .au via auDA) insert an extra "Contact" word - "Registrant
|
|
197
|
-
// Contact Email:", "Tech Contact Name:" - handled below with an optional
|
|
198
|
-
// "(?:Contact )?" group rather than a whole separate pattern, except for
|
|
199
|
-
// registrantName, where priority matters: on .au, the plain "Registrant:"
|
|
200
|
-
// line carries the actual legal entity ("Example Corporation Pty Ltd") while
|
|
201
|
-
// "Registrant Contact Name:" is often just a generic role ("Domain
|
|
202
|
-
// Administrator") - kept as a lower-priority third alternate so the more
|
|
203
|
-
// useful value wins when both are present.
|
|
204
|
-
registrantName: [
|
|
205
|
-
/^[ \t*]*Registrant Name[ \t.]*:[ \t]*(.+)$/im,
|
|
206
|
-
/^[ \t*]*Registrant[ \t.]*:[ \t]*(.+)$/im,
|
|
207
|
-
/^[ \t*]*Registrant Contact Name[ \t.]*:[ \t]*(.+)$/im,
|
|
208
|
-
/^[ \t*]*Owner Name[ \t.]*:[ \t]*(.+)$/im,
|
|
209
|
-
/^[ \t]*registrant_contact_name[ \t]*:[ \t]*(.+)$/im,
|
|
210
|
-
],
|
|
211
|
-
registrantId: [/^[ \t*]*(?:Registry )?Registrant ID[ \t.]*:[ \t]*(.+)$/im],
|
|
212
|
-
registrantOrg: [/^[ \t*]*Registrant (?:Contact )?Organi[sz]ation[ \t.]*:[ \t]*(.+)$/im],
|
|
213
|
-
registrantEmail: [
|
|
214
|
-
/^[ \t*]*Registrant (?:Contact )?Email[ \t.]*:[ \t]*(.+)$/im,
|
|
215
|
-
/^[ \t]*registrant_contact_email[ \t]*:[ \t]*(.+)$/im,
|
|
216
|
-
],
|
|
217
|
-
registrantPhone: [
|
|
218
|
-
/^[ \t*]*Registrant (?:Contact )?Phone[ \t.]*:[ \t]*(.+)$/im,
|
|
219
|
-
/^[ \t]*registrant_contact_phone[ \t]*:[ \t]*(.+)$/im,
|
|
220
|
-
],
|
|
221
|
-
registrantAddress: [
|
|
222
|
-
/^[ \t*]*Registrant (?:Contact )?Address[ \t.]*:[ \t]*(.+)$/im,
|
|
223
|
-
/^[ \t*]*Owner Address[ \t.]*:[ \t]*(.+)$/im,
|
|
224
|
-
],
|
|
225
|
-
registrantStreet: [/^[ \t*]*Registrant (?:Contact )?Street[ \t.]*:[ \t]*(.+)$/im],
|
|
226
|
-
registrantCity: [
|
|
227
|
-
/^[ \t*]*Registrant (?:Contact )?City[ \t.]*:[ \t]*(.+)$/im,
|
|
228
|
-
/^[ \t]*registrant_contact_city[ \t]*:[ \t]*(.+)$/im,
|
|
229
|
-
],
|
|
230
|
-
registrantState: [
|
|
231
|
-
/^[ \t*]*Registrant (?:Contact )?State(?:\/Province)?[ \t.]*:[ \t]*(.+)$/im,
|
|
232
|
-
/^[ \t]*registrant_contact_province[ \t]*:[ \t]*(.+)$/im,
|
|
233
|
-
],
|
|
234
|
-
registrantPostalCode: [
|
|
235
|
-
/^[ \t*]*Registrant (?:Contact )?Postal Code[ \t.]*:[ \t]*(.+)$/im,
|
|
236
|
-
/^[ \t]*registrant_contact_postalcode[ \t]*:[ \t]*(.+)$/im,
|
|
237
|
-
],
|
|
238
|
-
registrantCountry: [
|
|
239
|
-
/^[ \t*]*Registrant (?:Contact )?Country[ \t.]*:[ \t]*(.+)$/im,
|
|
240
|
-
/^[ \t*]*Owner Country[ \t.]*:[ \t]*(.+)$/im,
|
|
241
|
-
/^[ \t]*registrant_contact_country[ \t]*:[ \t]*(.+)$/im,
|
|
242
|
-
],
|
|
243
|
-
adminId: [/^[ \t*]*(?:Registry )?Admin(?:istrative)? (?:Contact )?ID[ \t.]*:[ \t]*(.+)$/im],
|
|
244
|
-
adminName: [
|
|
245
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?Name[ \t.]*:[ \t]*(.+)$/im,
|
|
246
|
-
/^[ \t]*admin_contact_name[ \t]*:[ \t]*(.+)$/im,
|
|
247
|
-
],
|
|
248
|
-
adminOrg: [/^[ \t*]*Admin(?:istrative)? (?:Contact )?Organi[sz]ation[ \t.]*:[ \t]*(.+)$/im],
|
|
249
|
-
adminEmail: [
|
|
250
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?Email[ \t.]*:[ \t]*(.+)$/im,
|
|
251
|
-
/^[ \t]*admin_contact_email[ \t]*:[ \t]*(.+)$/im,
|
|
252
|
-
],
|
|
253
|
-
adminPhone: [
|
|
254
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?Phone[ \t.]*:[ \t]*(.+)$/im,
|
|
255
|
-
/^[ \t]*admin_contact_phone[ \t]*:[ \t]*(.+)$/im,
|
|
256
|
-
],
|
|
257
|
-
adminAddress: [/^[ \t*]*Admin(?:istrative)? (?:Contact )?Address[ \t.]*:[ \t]*(.+)$/im],
|
|
258
|
-
adminStreet: [/^[ \t*]*Admin(?:istrative)? (?:Contact )?Street[ \t.]*:[ \t]*(.+)$/im],
|
|
259
|
-
adminCity: [
|
|
260
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?City[ \t.]*:[ \t]*(.+)$/im,
|
|
261
|
-
/^[ \t]*admin_contact_city[ \t]*:[ \t]*(.+)$/im,
|
|
262
|
-
],
|
|
263
|
-
adminState: [
|
|
264
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?State(?:\/Province)?[ \t.]*:[ \t]*(.+)$/im,
|
|
265
|
-
/^[ \t]*admin_contact_province[ \t]*:[ \t]*(.+)$/im,
|
|
266
|
-
],
|
|
267
|
-
adminPostalCode: [
|
|
268
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?Postal Code[ \t.]*:[ \t]*(.+)$/im,
|
|
269
|
-
/^[ \t]*admin_contact_postalcode[ \t]*:[ \t]*(.+)$/im,
|
|
270
|
-
],
|
|
271
|
-
adminCountry: [
|
|
272
|
-
/^[ \t*]*Admin(?:istrative)? (?:Contact )?Country[ \t.]*:[ \t]*(.+)$/im,
|
|
273
|
-
/^[ \t]*admin_contact_country[ \t]*:[ \t]*(.+)$/im,
|
|
274
|
-
],
|
|
275
|
-
techId: [/^[ \t*]*(?:Registry )?Tech(?:nical)? (?:Contact )?ID[ \t.]*:[ \t]*(.+)$/im],
|
|
276
|
-
techName: [
|
|
277
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?Name[ \t.]*:[ \t]*(.+)$/im,
|
|
278
|
-
/^[ \t]*technical_contact_name[ \t]*:[ \t]*(.+)$/im,
|
|
279
|
-
],
|
|
280
|
-
techOrg: [/^[ \t*]*Tech(?:nical)? (?:Contact )?Organi[sz]ation[ \t.]*:[ \t]*(.+)$/im],
|
|
281
|
-
techEmail: [
|
|
282
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?Email[ \t.]*:[ \t]*(.+)$/im,
|
|
283
|
-
/^[ \t]*technical_contact_email[ \t]*:[ \t]*(.+)$/im,
|
|
284
|
-
],
|
|
285
|
-
techPhone: [
|
|
286
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?Phone[ \t.]*:[ \t]*(.+)$/im,
|
|
287
|
-
/^[ \t]*technical_contact_phone[ \t]*:[ \t]*(.+)$/im,
|
|
288
|
-
],
|
|
289
|
-
techAddress: [/^[ \t*]*Tech(?:nical)? (?:Contact )?Address[ \t.]*:[ \t]*(.+)$/im],
|
|
290
|
-
techStreet: [/^[ \t*]*Tech(?:nical)? (?:Contact )?Street[ \t.]*:[ \t]*(.+)$/im],
|
|
291
|
-
techCity: [
|
|
292
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?City[ \t.]*:[ \t]*(.+)$/im,
|
|
293
|
-
/^[ \t]*technical_contact_city[ \t]*:[ \t]*(.+)$/im,
|
|
294
|
-
],
|
|
295
|
-
techState: [
|
|
296
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?State(?:\/Province)?[ \t.]*:[ \t]*(.+)$/im,
|
|
297
|
-
/^[ \t]*technical_contact_province[ \t]*:[ \t]*(.+)$/im,
|
|
298
|
-
],
|
|
299
|
-
techPostalCode: [
|
|
300
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?Postal Code[ \t.]*:[ \t]*(.+)$/im,
|
|
301
|
-
/^[ \t]*technical_contact_postalcode[ \t]*:[ \t]*(.+)$/im,
|
|
302
|
-
],
|
|
303
|
-
techCountry: [
|
|
304
|
-
/^[ \t*]*Tech(?:nical)? (?:Contact )?Country[ \t.]*:[ \t]*(.+)$/im,
|
|
305
|
-
/^[ \t]*technical_contact_country[ \t]*:[ \t]*(.+)$/im,
|
|
306
|
-
],
|
|
307
|
-
billingId: [/^[ \t*]*(?:Registry )?Billing (?:Contact )?ID[ \t.]*:[ \t]*(.+)$/im],
|
|
308
|
-
billingName: [/^[ \t*]*Billing (?:Contact )?Name[ \t.]*:[ \t]*(.+)$/im],
|
|
309
|
-
billingOrg: [/^[ \t*]*Billing (?:Contact )?Organi[sz]ation[ \t.]*:[ \t]*(.+)$/im],
|
|
310
|
-
billingEmail: [/^[ \t*]*Billing (?:Contact )?Email[ \t.]*:[ \t]*(.+)$/im],
|
|
311
|
-
billingPhone: [/^[ \t*]*Billing (?:Contact )?Phone[ \t.]*:[ \t]*(.+)$/im],
|
|
312
|
-
billingAddress: [/^[ \t*]*Billing (?:Contact )?Address[ \t.]*:[ \t]*(.+)$/im],
|
|
313
|
-
billingStreet: [/^[ \t*]*Billing (?:Contact )?Street[ \t.]*:[ \t]*(.+)$/im],
|
|
314
|
-
billingCity: [/^[ \t*]*Billing (?:Contact )?City[ \t.]*:[ \t]*(.+)$/im],
|
|
315
|
-
billingState: [/^[ \t*]*Billing (?:Contact )?State(?:\/Province)?[ \t.]*:[ \t]*(.+)$/im],
|
|
316
|
-
billingPostalCode: [/^[ \t*]*Billing (?:Contact )?Postal Code[ \t.]*:[ \t]*(.+)$/im],
|
|
317
|
-
billingCountry: [/^[ \t*]*Billing (?:Contact )?Country[ \t.]*:[ \t]*(.+)$/im],
|
|
318
|
-
};
|
|
319
|
-
const nameservers = new Set();
|
|
320
|
-
const statuses = new Set();
|
|
321
|
-
source.forEach((hop, hopIndex) => {
|
|
322
|
-
const text = hop.response;
|
|
323
|
-
if (!text)
|
|
324
|
-
return;
|
|
325
|
-
// hopIndex 0 is always whois.iana.org, whose "domain:"/"created:"/
|
|
326
|
-
// "changed:" fields describe the TLD's own root delegation record, not
|
|
327
|
-
// the queried domain - e.g. "created: 1992-08-14" for .gt is Guatemala's
|
|
328
|
-
// delegation date, not any individual .gt domain's registration date.
|
|
329
|
-
// The broadened alternate labels (everything past index 0 in each
|
|
330
|
-
// pattern array) are common enough on real registries' per-domain
|
|
331
|
-
// responses that they'd false-match that IANA hop too, so they're only
|
|
332
|
-
// tried against later, registry-level hops.
|
|
333
|
-
const isRootHop = hopIndex === 0;
|
|
334
|
-
const isDnsBelgium = !isRootHop
|
|
335
|
-
&& /^%[^\r\n]*\.be Whois Server/im.test(text)
|
|
336
|
-
&& /^[ \t]*Registered[ \t]*:/im.test(text);
|
|
337
|
-
const isEurid = !isRootHop
|
|
338
|
-
&& /^[ \t]*Script[ \t]*:/im.test(text)
|
|
339
|
-
&& /(?:^|\s)(?:www\.)?eurid\.eu(?:\s|\/|$)/im.test(text);
|
|
340
|
-
const isNorid = !isRootHop
|
|
341
|
-
&& /^[ \t]*Domain Information[ \t]*$/im.test(text)
|
|
342
|
-
&& /^[ \t]*NORID Handle[ \t.]*:/im.test(text)
|
|
343
|
-
&& /^[ \t]*Registrar Handle[ \t.]*:/im.test(text)
|
|
344
|
-
&& /^[ \t]*Additional information[ \t]*:/im.test(text);
|
|
345
|
-
const isCnnic = !isRootHop
|
|
346
|
-
&& /^[ \t]*ROID[ \t]*:/im.test(text)
|
|
347
|
-
&& /^[ \t]*Sponsoring Registrar[ \t]*:/im.test(text)
|
|
348
|
-
&& /^[ \t]*Registration Time[ \t]*:/im.test(text)
|
|
349
|
-
&& /^[ \t]*Expiration Time[ \t]*:/im.test(text);
|
|
350
|
-
const isPunktum = !isRootHop
|
|
351
|
-
&& /^[ \t]*Registration period[ \t]*:/im.test(text)
|
|
352
|
-
&& /^[ \t]*VID[ \t]*:/im.test(text)
|
|
353
|
-
&& /^[ \t]*Nameservers[ \t]*$/im.test(text);
|
|
354
|
-
const isPandi = !isRootHop
|
|
355
|
-
&& /^[ \t]*Domain ID[ \t]*:/im.test(text)
|
|
356
|
-
&& /^[ \t]*Sponsoring Registrar Organization[ \t]*:/im.test(text);
|
|
357
|
-
const isIsocIl = !isRootHop
|
|
358
|
-
&& /^[ \t]*query[ \t]*:/im.test(text)
|
|
359
|
-
&& /^[ \t]*reg-name[ \t]*:/im.test(text)
|
|
360
|
-
&& /^[ \t]*validity[ \t]*:/im.test(text);
|
|
361
|
-
const isTwnic = !isRootHop
|
|
362
|
-
&& /^[ \t]*Record created on[ \t]*:/im.test(text)
|
|
363
|
-
&& /^[ \t]*Record expires on[ \t]*:/im.test(text)
|
|
364
|
-
&& /^[ \t]*Registration Service Provider[ \t]*:/im.test(text);
|
|
365
|
-
const isRegisterBg = !isRootHop
|
|
366
|
-
&& /^[ \t]*registration status[ \t]*:/im.test(text)
|
|
367
|
-
&& /^[ \t]*NAME SERVER INFORMATION[ \t]*:[ \t]*$/im.test(text);
|
|
368
|
-
const isEif = !isRootHop
|
|
369
|
-
&& /Estonia \.ee Top Level Domain WHOIS server/i.test(text)
|
|
370
|
-
&& /^[ \t]*Domain[ \t]*:[ \t]*$/im.test(text)
|
|
371
|
-
&& /^[ \t]*Name servers[ \t]*:[ \t]*$/im.test(text);
|
|
372
|
-
const isIszt = !isRootHop
|
|
373
|
-
&& /^% Whois server[^\r\n]*hu ccTLD/im.test(text)
|
|
374
|
-
&& /^[ \t]*record created[ \t]*:/im.test(text);
|
|
375
|
-
const isIsnic = !isRootHop
|
|
376
|
-
&& /^% This is the ISNIC Whois server\./im.test(text)
|
|
377
|
-
&& /^[ \t]*source[ \t]*:[ \t]*ISNIC(?:\s|$)/im.test(text);
|
|
378
|
-
const isNicLv = !isRootHop
|
|
379
|
-
&& /^[ \t]*\[Domain\][ \t]*$/im.test(text)
|
|
380
|
-
&& /^[ \t]*\[Holder\][ \t]*$/im.test(text)
|
|
381
|
-
&& /^[ \t]*\[Whois\][ \t]*$/im.test(text);
|
|
382
|
-
const isSidn = !isRootHop
|
|
383
|
-
&& /^[ \t]*Domain nameservers[ \t]*:[ \t]*$/im.test(text)
|
|
384
|
-
&& /^[ \t]*Abuse Contact[ \t]*:[ \t]*$/im.test(text)
|
|
385
|
-
&& /^[ \t]*Record maintained by[ \t]*:/im.test(text);
|
|
386
|
-
const isRnids = !isRootHop
|
|
387
|
-
&& /^[ \t]*Registration date[ \t]*:/im.test(text)
|
|
388
|
-
&& /^[ \t]*Modification date[ \t]*:/im.test(text)
|
|
389
|
-
&& /^[ \t]*DNSSEC signed[ \t]*:/im.test(text);
|
|
390
|
-
const isCctldBy = !isRootHop
|
|
391
|
-
&& /^[ \t]*Domain name[ \t]*:/im.test(text)
|
|
392
|
-
&& /^[ \t]*Registration or other identification number[ \t]*:/im.test(text);
|
|
393
|
-
const isHkirc = !isRootHop
|
|
394
|
-
&& /^[ \t]*Domain Name Information[ \t]*:[ \t]*$/im.test(text)
|
|
395
|
-
&& /^[ \t]*Name Servers Information[ \t]*:[ \t]*$/im.test(text)
|
|
396
|
-
&& /^[ \t]*Domain Name Commencement Date[ \t]*:/im.test(text);
|
|
397
|
-
const isIrnic = !isRootHop
|
|
398
|
-
&& /^[ \t]*source[ \t]*:[ \t]*IRNIC(?:\s|$)/im.test(text)
|
|
399
|
-
&& /^[ \t]*holder-c[ \t]*:/im.test(text)
|
|
400
|
-
&& /^[ \t]*nic-hdl[ \t]*:/im.test(text);
|
|
401
|
-
const isNicKz = !isRootHop
|
|
402
|
-
&& /^[ \t]*Domain Name[ \t.]*:/im.test(text)
|
|
403
|
-
&& /^[ \t]*Current Registar[ \t]*:/im.test(text)
|
|
404
|
-
&& /^[ \t]*Primary server[ \t.]*:/im.test(text);
|
|
405
|
-
const isDnsLu = !isRootHop
|
|
406
|
-
&& /^[ \t]*domainname[ \t]*:/im.test(text)
|
|
407
|
-
&& /^[ \t]*domaintype[ \t]*:/im.test(text)
|
|
408
|
-
&& /^[ \t]*registrar-name[ \t]*:/im.test(text);
|
|
409
|
-
const isNicMd = !isRootHop
|
|
410
|
-
&& /^[ \t]*Domain[ \t]+name[ \t]*:/im.test(text)
|
|
411
|
-
&& /^[ \t]*Domain state[ \t]*:/im.test(text)
|
|
412
|
-
&& /^[ \t]*Registered on[ \t]*:/im.test(text);
|
|
413
|
-
const isThnic = !isRootHop
|
|
414
|
-
&& /^[ \t]*Domain Holder Organization[ \t]*:/im.test(text)
|
|
415
|
-
&& /^[ \t]*Created date[ \t]*:/im.test(text)
|
|
416
|
-
&& /^[ \t]*Exp date[ \t]*:/im.test(text);
|
|
417
|
-
const isAtiTn = !isRootHop
|
|
418
|
-
&& /^[ \t]*NIC Whois server for cTLDs[ \t.]*:/im.test(text)
|
|
419
|
-
&& /^[ \t]*Domain name[ \t.]*:/im.test(text)
|
|
420
|
-
&& /^[ \t]*Creation date[ \t.]*:/im.test(text);
|
|
421
|
-
const isMonic = !isRootHop
|
|
422
|
-
&& /^%[ \t]*Monic Whois Server Version[ \t]+\d/im.test(text)
|
|
423
|
-
&& /^[ \t]*Domain Name[ \t]*:/im.test(text)
|
|
424
|
-
&& /^[ \t]*Record created on[ \t]+[^:\r\n]/im.test(text)
|
|
425
|
-
&& /^[ \t]*Domain name servers[ \t]*:[ \t]*$/im.test(text);
|
|
426
|
-
const isNicKg = !isRootHop && hasNicKgRegistrationEvidence(text);
|
|
427
|
-
for (const [key, res] of Object.entries(patterns)) {
|
|
428
|
-
// IANA's root hop describes the TLD and its operator, never a contact
|
|
429
|
-
// for the queried registrable domain. Domain contacts therefore come
|
|
430
|
-
// only from registry/registrar hops.
|
|
431
|
-
if (isRootHop && /^(?:registrant|admin|tech|billing)/.test(key))
|
|
432
|
-
continue;
|
|
433
|
-
if (fields[key])
|
|
434
|
-
continue; // earlier hop already set it - don't let a later, less-authoritative hop overwrite
|
|
435
|
-
const candidates = isRootHop ? res.slice(0, 1) : res;
|
|
436
|
-
for (const re of candidates) {
|
|
437
|
-
const m = text.match(re);
|
|
438
|
-
const matchedValue = m?.[1];
|
|
439
|
-
if (matchedValue) {
|
|
440
|
-
const value = matchedValue.trim();
|
|
441
|
-
// Some WHOIS formats use "Registered: yes/no" as a boolean state,
|
|
442
|
-
// while others use "Registered: <date>" for creation time. Never
|
|
443
|
-
// store the boolean form as a date ("no" previously became a truthy
|
|
444
|
-
// createdDate and could make availability look registered).
|
|
445
|
-
if (key === 'createdDate' && /^(?:yes|no|true|false|available|free)$/i.test(value))
|
|
446
|
-
continue;
|
|
447
|
-
const bounded = boundedWhoisValue(value, whoisFieldLimit(key));
|
|
448
|
-
if (!bounded.value)
|
|
449
|
-
continue;
|
|
450
|
-
fields[key] = bounded.value;
|
|
451
|
-
if (bounded.truncated)
|
|
452
|
-
truncatedFields.add(key);
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
// A small set of ccTLD dialects reuse terse labels that would be unsafe
|
|
458
|
-
// as global aliases (for example `org:` and `state:` also occur inside
|
|
459
|
-
// contact/address blocks). Gate those fields on registry-specific marker
|
|
460
|
-
// combinations, while retaining the same scalar/list bounds as the
|
|
461
|
-
// generic parser and leaving endpoint discovery and authority untouched.
|
|
462
|
-
if (!isRootHop) {
|
|
463
|
-
if (isRegisterBg) {
|
|
464
|
-
const domainLine = text.match(/^[ \t]*DOMAIN NAME[ \t]*:[ \t]*([^\r\n]+)/im);
|
|
465
|
-
const rawDomainLine = domainLine?.[1];
|
|
466
|
-
if (rawDomainLine) {
|
|
467
|
-
const boundedLine = rawDomainLine.slice(0, MAX_WHOIS_FIELD_LENGTH);
|
|
468
|
-
const parenthesized = boundedLine.match(/\(([a-z0-9.-]{1,253})\)[ \t]*$/i)?.[1] || '';
|
|
469
|
-
const leadingAscii = boundedLine.match(/^([a-z0-9.-]{1,253})(?:[ \t]|$)/i)?.[1] || '';
|
|
470
|
-
const unicodeDomain = boundedLine.replace(/[ \t]+\([^\r\n()]{1,253}\)[ \t]*$/, '').trim();
|
|
471
|
-
const domain = [parenthesized, leadingAscii, domainToASCII(unicodeDomain)]
|
|
472
|
-
.find((candidate) => WHOIS_DOMAIN_RE.test(candidate));
|
|
473
|
-
const bounded = boundedWhoisValue(domain, whoisFieldLimit('domainName'));
|
|
474
|
-
if (bounded.value)
|
|
475
|
-
fields.domainName = bounded.value;
|
|
476
|
-
if (bounded.truncated)
|
|
477
|
-
truncatedFields.add('domainName');
|
|
478
|
-
}
|
|
479
|
-
for (const match of text.matchAll(/^[ \t]*registration status[ \t]*:[ \t]*([^\r\n]+)/gim)) {
|
|
480
|
-
if (addBoundedWhoisSetValue(statuses, match[1], {
|
|
481
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
482
|
-
field: 'statuses', truncatedFields,
|
|
483
|
-
}) === 'capped')
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
collectBareWhoisNameservers(text, /^[ \t]*NAME SERVER INFORMATION[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
|
|
487
|
-
}
|
|
488
|
-
if (isEif) {
|
|
489
|
-
const domainSection = parseBoundedWhoisSection(text, /^[ \t]*Domain[ \t]*:[ \t]*$/im);
|
|
490
|
-
const registrantSection = parseBoundedWhoisSection(text, /^[ \t]*Registrant[ \t]*:[ \t]*$/im);
|
|
491
|
-
const adminSection = parseBoundedWhoisSection(text, /^[ \t]*Administrative contact[ \t]*:[ \t]*$/im);
|
|
492
|
-
const techSection = parseBoundedWhoisSection(text, /^[ \t]*Technical contact[ \t]*:[ \t]*$/im);
|
|
493
|
-
const registrarSection = parseBoundedWhoisSection(text, /^[ \t]*Registrar[ \t]*:[ \t]*$/im);
|
|
494
|
-
assignBoundedWhoisMatch(domainSection, fields, 'domainName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
495
|
-
assignBoundedWhoisMatch(registrantSection, fields, 'registrantName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
496
|
-
assignBoundedWhoisMatch(registrantSection, fields, 'registrantId', /^[ \t]*org id[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
497
|
-
assignBoundedWhoisMatch(registrantSection, fields, 'registrantCountry', /^[ \t]*country[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
498
|
-
assignBoundedWhoisMatch(adminSection, fields, 'adminName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
499
|
-
assignBoundedWhoisMatch(techSection, fields, 'techName', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
500
|
-
assignBoundedWhoisMatch(registrarSection, fields, 'registrar', /^[ \t]*name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
501
|
-
assignBoundedWhoisMatch(registrarSection, fields, 'registrarUrl', /^[ \t]*url[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
502
|
-
}
|
|
503
|
-
if (isIszt) {
|
|
504
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*record created[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
505
|
-
}
|
|
506
|
-
if (isIsnic) {
|
|
507
|
-
const handle = text.match(/^[ \t]*registrant[ \t]*:[ \t]*(.+)$/im)?.[1] || null;
|
|
508
|
-
if (handle) {
|
|
509
|
-
const boundedHandle = boundedWhoisValue(handle, whoisFieldLimit('registrantId'));
|
|
510
|
-
if (boundedHandle.value)
|
|
511
|
-
fields.registrantId = boundedHandle.value;
|
|
512
|
-
if (boundedHandle.truncated)
|
|
513
|
-
truncatedFields.add('registrantId');
|
|
514
|
-
delete fields.registrantName;
|
|
515
|
-
const role = resolveIsnicRole(text, boundedHandle.value);
|
|
516
|
-
if (role) {
|
|
517
|
-
if (role.org.value)
|
|
518
|
-
fields.registrantOrg = role.org.value;
|
|
519
|
-
if (role.email.value)
|
|
520
|
-
fields.registrantEmail = role.email.value;
|
|
521
|
-
if (role.phone.value)
|
|
522
|
-
fields.registrantPhone = role.phone.value;
|
|
523
|
-
if (role.address.value)
|
|
524
|
-
fields.registrantAddress = role.address.value;
|
|
525
|
-
if (role.org.truncated)
|
|
526
|
-
truncatedFields.add('registrantOrg');
|
|
527
|
-
if (role.email.truncated)
|
|
528
|
-
truncatedFields.add('registrantEmail');
|
|
529
|
-
if (role.phone.truncated)
|
|
530
|
-
truncatedFields.add('registrantPhone');
|
|
531
|
-
if (role.address.truncated || role.truncated)
|
|
532
|
-
truncatedFields.add('registrantAddress');
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
536
|
-
assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
537
|
-
assignBoundedWhoisMatch(text, fields, 'billingId', /^[ \t]*billing-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
538
|
-
}
|
|
539
|
-
if (isNicLv) {
|
|
540
|
-
const holderSection = parseBoundedWhoisSection(text, /^[ \t]*\[Holder\][ \t]*$/im);
|
|
541
|
-
assignBoundedWhoisMatch(holderSection, fields, 'registrantName', /^[ \t]*Name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
542
|
-
assignBoundedWhoisMatch(holderSection, fields, 'registrantId', /^[ \t]*RegNr[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
543
|
-
assignBoundedWhoisMatch(holderSection, fields, 'registrantAddress', /^[ \t]*Address[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
544
|
-
assignBoundedWhoisMatch(holderSection, fields, 'registrantCountry', /^[ \t]*Country[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
545
|
-
}
|
|
546
|
-
if (isSidn) {
|
|
547
|
-
const registrar = parseIndentedWhoisValue(text, /^[ \t]*Registrar[ \t]*:[ \t]*$/im, whoisFieldLimit('registrar'));
|
|
548
|
-
if (registrar?.value)
|
|
549
|
-
fields.registrar = registrar.value;
|
|
550
|
-
if (registrar?.truncated)
|
|
551
|
-
truncatedFields.add('registrar');
|
|
552
|
-
const abuse = parseIndentedContactBlock(text, /^[ \t]*Abuse Contact[ \t]*:[ \t]*$/im);
|
|
553
|
-
if (abuse?.email)
|
|
554
|
-
fields.abuseEmail = abuse.email;
|
|
555
|
-
if (abuse?.phone)
|
|
556
|
-
fields.abusePhone = abuse.phone;
|
|
557
|
-
if (abuse?.truncated) {
|
|
558
|
-
truncatedFields.add('abuseEmail');
|
|
559
|
-
truncatedFields.add('abusePhone');
|
|
560
|
-
}
|
|
561
|
-
collectBareWhoisNameservers(text, /^[ \t]*Domain nameservers[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
|
|
562
|
-
}
|
|
563
|
-
if (isRnids) {
|
|
564
|
-
assignBoundedWhoisMatch(text, fields, 'adminName', /^[ \t]*Administrative contact[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
565
|
-
assignBoundedWhoisMatch(text, fields, 'techName', /^[ \t]*Technical contact[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
566
|
-
assignBoundedWhoisMatch(text, fields, 'dnssec', /^[ \t]*DNSSEC signed[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
567
|
-
for (const match of text.matchAll(/^[ \t]*Domain status[ \t]*:[ \t]*(.*?)(?:[ \t]+https?:\/\/\S+)?[ \t]*$/gim)) {
|
|
568
|
-
if (addBoundedWhoisSetValue(statuses, match[1], {
|
|
569
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
570
|
-
field: 'statuses', truncatedFields,
|
|
571
|
-
}) === 'capped')
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (isCctldBy) {
|
|
576
|
-
assignBoundedWhoisMatch(text, fields, 'registrantOrg', /^[ \t]*Org[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
577
|
-
assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*Registration or other identification number[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
578
|
-
}
|
|
579
|
-
if (isHkirc) {
|
|
580
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Domain Name Commencement Date[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
581
|
-
collectBareWhoisNameservers(text, /^[ \t]*Name Servers Information[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
|
|
582
|
-
}
|
|
583
|
-
if (isIrnic) {
|
|
584
|
-
const registrantId = text.match(/^[ \t]*holder-c[ \t]*:[ \t]*(.+)$/im)?.[1] || null;
|
|
585
|
-
if (registrantId) {
|
|
586
|
-
assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*holder-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
587
|
-
const contact = resolveIrnicContact(text, fields.registrantId);
|
|
588
|
-
if (contact) {
|
|
589
|
-
if (contact.name.value)
|
|
590
|
-
fields.registrantName = contact.name.value;
|
|
591
|
-
if (contact.org.value)
|
|
592
|
-
fields.registrantOrg = contact.org.value;
|
|
593
|
-
if (contact.email.value)
|
|
594
|
-
fields.registrantEmail = contact.email.value;
|
|
595
|
-
if (contact.phone.value)
|
|
596
|
-
fields.registrantPhone = contact.phone.value;
|
|
597
|
-
if (contact.address.value)
|
|
598
|
-
fields.registrantAddress = contact.address.value;
|
|
599
|
-
if (contact.name.truncated)
|
|
600
|
-
truncatedFields.add('registrantName');
|
|
601
|
-
if (contact.org.truncated)
|
|
602
|
-
truncatedFields.add('registrantOrg');
|
|
603
|
-
if (contact.email.truncated)
|
|
604
|
-
truncatedFields.add('registrantEmail');
|
|
605
|
-
if (contact.phone.truncated)
|
|
606
|
-
truncatedFields.add('registrantPhone');
|
|
607
|
-
if (contact.address.truncated || contact.truncated)
|
|
608
|
-
truncatedFields.add('registrantAddress');
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
612
|
-
assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
613
|
-
assignBoundedWhoisMatch(text, fields, 'billingId', /^[ \t]*bill-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
614
|
-
}
|
|
615
|
-
if (isNicKz) {
|
|
616
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Domain created[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
617
|
-
assignBoundedWhoisMatch(text, fields, 'updatedDate', /^[ \t]*Last modified[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
|
|
618
|
-
assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Current Registar[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
619
|
-
for (const pattern of [
|
|
620
|
-
/^[ \t]*Primary server[ \t.]*:[ \t]*([a-zA-Z0-9.-]+)/gim,
|
|
621
|
-
/^[ \t]*Secondary server[ \t.]*:[ \t]*([a-zA-Z0-9.-]+)/gim,
|
|
622
|
-
]) {
|
|
623
|
-
for (const match of text.matchAll(pattern)) {
|
|
624
|
-
const result = addBoundedWhoisSetValue(nameservers, match[1], {
|
|
625
|
-
maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
|
|
626
|
-
field: 'nameservers', truncatedFields,
|
|
627
|
-
});
|
|
628
|
-
if (result === 'capped')
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if (isDnsLu) {
|
|
634
|
-
assignBoundedWhoisMatch(text, fields, 'domainName', /^[ \t]*domainname[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
635
|
-
assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*registrar-name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
636
|
-
assignBoundedWhoisMatch(text, fields, 'registrarUrl', /^[ \t]*registrar-url[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
637
|
-
}
|
|
638
|
-
if (isNicMd) {
|
|
639
|
-
assignBoundedWhoisMatch(text, fields, 'domainName', /^[ \t]*Domain[ \t]+name[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
640
|
-
for (const match of text.matchAll(/^[ \t]*Domain state[ \t]*:[ \t]*([^\r\n]+)/gim)) {
|
|
641
|
-
if (addBoundedWhoisSetValue(statuses, match[1], {
|
|
642
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
643
|
-
field: 'statuses', truncatedFields,
|
|
644
|
-
}) === 'capped')
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (isThnic) {
|
|
649
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Created date[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
650
|
-
assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Exp date[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
651
|
-
assignBoundedWhoisMatch(text, fields, 'registrantOrg', /^[ \t]*Domain Holder Organization[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
652
|
-
assignBoundedWhoisMatch(text, fields, 'registrantCountry', /^[ \t]*Domain Holder Country[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
653
|
-
assignBoundedWhoisMatch(text, fields, 'registrantStreet', /^[ \t]*Domain Holder Street[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
654
|
-
}
|
|
655
|
-
if (isMonic) {
|
|
656
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Record created on[ \t]+(.+)$/im, truncatedFields);
|
|
657
|
-
collectBareWhoisNameservers(text, /^[ \t]*Domain name servers[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
|
|
658
|
-
}
|
|
659
|
-
if (isNicKg) {
|
|
660
|
-
const domainHeader = text.match(/^[ \t]*Domain[ \t]+([a-z0-9.-]+)[ \t]+\(([A-Z][A-Z0-9_-]*)\)[ \t]*$/im);
|
|
661
|
-
if (domainHeader) {
|
|
662
|
-
const domain = boundedWhoisValue(domainHeader[1], whoisFieldLimit('domainName'));
|
|
663
|
-
if (domain.value)
|
|
664
|
-
fields.domainName = domain.value;
|
|
665
|
-
if (domain.truncated)
|
|
666
|
-
truncatedFields.add('domainName');
|
|
667
|
-
addBoundedWhoisSetValue(statuses, domainHeader[2], {
|
|
668
|
-
maxEntries: MAX_WHOIS_STATUSES,
|
|
669
|
-
maxLength: 160,
|
|
670
|
-
field: 'statuses',
|
|
671
|
-
truncatedFields,
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Record created[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
675
|
-
assignBoundedWhoisMatch(text, fields, 'updatedDate', /^[ \t]*Record last updated on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
676
|
-
assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Record expires on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
677
|
-
collectBareWhoisNameservers(text, /^[ \t]*Name servers in the listed order[ \t]*:[ \t]*$/im, nameservers, truncatedFields);
|
|
678
|
-
}
|
|
679
|
-
const isRegistroBr = /^[ \t]*owner-c[ \t]*:/im.test(text)
|
|
680
|
-
&& /^[ \t]*country[ \t]*:[ \t]*BR(?:\s|$)/im.test(text);
|
|
681
|
-
if (isRegistroBr) {
|
|
682
|
-
assignBoundedWhoisMatch(text, fields, 'registrantName', /^[ \t]*owner[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
683
|
-
assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*owner-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
684
|
-
assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
685
|
-
assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
686
|
-
}
|
|
687
|
-
const isAfnic = /^[ \t]*source[ \t]*:[ \t]*FRNIC(?:\s|$)/im.test(text);
|
|
688
|
-
if (isAfnic) {
|
|
689
|
-
assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*holder-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
690
|
-
assignBoundedWhoisMatch(text, fields, 'adminId', /^[ \t]*admin-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
691
|
-
assignBoundedWhoisMatch(text, fields, 'techId', /^[ \t]*tech-c[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
692
|
-
for (const match of text.matchAll(/^[ \t]*eppstatus[ \t]*:[ \t]*(.+)$/gim)) {
|
|
693
|
-
if (addBoundedWhoisSetValue(statuses, match[1], {
|
|
694
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
695
|
-
field: 'statuses', truncatedFields,
|
|
696
|
-
}) === 'capped')
|
|
697
|
-
break;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
const isTci = /^[ \t]*source[ \t]*:[ \t]*TCI(?:\s|$)/im.test(text)
|
|
701
|
-
&& /^[ \t]*paid-till[ \t]*:/im.test(text);
|
|
702
|
-
if (isTci) {
|
|
703
|
-
assignBoundedWhoisMatch(text, fields, 'registrantOrg', /^[ \t]*org[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
704
|
-
for (const match of text.matchAll(/^[ \t]*state[ \t]*:[ \t]*(.+)$/gim)) {
|
|
705
|
-
if (addBoundedWhoisSetValue(statuses, match[1], {
|
|
706
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
707
|
-
field: 'statuses', truncatedFields,
|
|
708
|
-
}) === 'capped')
|
|
709
|
-
break;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
const isInternetstiftelsen = /^[ \t]*registry-lock[ \t]*:/im.test(text)
|
|
713
|
-
&& /^[ \t]*holder[ \t]*:/im.test(text);
|
|
714
|
-
if (isInternetstiftelsen) {
|
|
715
|
-
assignBoundedWhoisMatch(text, fields, 'registrantId', /^[ \t]*holder[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
716
|
-
for (const match of text.matchAll(/^[ \t]*state[ \t]*:[ \t]*(.+)$/gim)) {
|
|
717
|
-
if (addBoundedWhoisSetValue(statuses, match[1], {
|
|
718
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
719
|
-
field: 'statuses', truncatedFields,
|
|
720
|
-
}) === 'capped')
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
const isNask = /^[ \t]*DOMAIN NAME[ \t]*:/im.test(text)
|
|
725
|
-
&& /^[ \t]*registrant type[ \t]*:/im.test(text)
|
|
726
|
-
&& /^[ \t]*renewal date[ \t]*:/im.test(text)
|
|
727
|
-
&& /^[ \t]*REGISTRAR[ \t]*:[ \t]*$/im.test(text);
|
|
728
|
-
if (isNask) {
|
|
729
|
-
if (!fields.registrar) {
|
|
730
|
-
const registrarHeader = text.match(/^[ \t]*REGISTRAR[ \t]*:[ \t]*$/im);
|
|
731
|
-
const followingLines = registrarHeader
|
|
732
|
-
? text.slice((registrarHeader.index ?? 0) + registrarHeader[0].length).split('\n').slice(0, 8)
|
|
733
|
-
: [];
|
|
734
|
-
for (const line of followingLines) {
|
|
735
|
-
if (!line.trim())
|
|
736
|
-
continue;
|
|
737
|
-
const bounded = boundedWhoisValue(line, whoisFieldLimit('registrar'));
|
|
738
|
-
if (bounded.value && !/^[a-z][a-z0-9+.-]*:\/\//i.test(bounded.value)
|
|
739
|
-
&& !/^[a-z][a-z0-9 -]{0,50}:[ \t]/i.test(bounded.value)) {
|
|
740
|
-
fields.registrar = bounded.value;
|
|
741
|
-
}
|
|
742
|
-
if (bounded.truncated)
|
|
743
|
-
truncatedFields.add('registrar');
|
|
744
|
-
break;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
const nameserverHeader = text.match(/^[ \t]*nameservers[ \t]*:[ \t]*(.*)$/im);
|
|
748
|
-
if (nameserverHeader) {
|
|
749
|
-
const candidates = [
|
|
750
|
-
nameserverHeader[1] ?? '',
|
|
751
|
-
...text.slice((nameserverHeader.index ?? 0) + nameserverHeader[0].length)
|
|
752
|
-
.split('\n').slice(1, MAX_WHOIS_NAMESERVERS + 2),
|
|
753
|
-
];
|
|
754
|
-
let found = 0;
|
|
755
|
-
for (const line of candidates) {
|
|
756
|
-
if (line === undefined)
|
|
757
|
-
continue;
|
|
758
|
-
const trimmed = line.trim();
|
|
759
|
-
if (!trimmed) {
|
|
760
|
-
if (found > 0)
|
|
761
|
-
break;
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
const hostMatch = trimmed.match(/^([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\.?(?:\s|$)/);
|
|
765
|
-
if (!hostMatch)
|
|
766
|
-
break;
|
|
767
|
-
const hostname = hostMatch[1];
|
|
768
|
-
if (!hostname)
|
|
769
|
-
break;
|
|
770
|
-
const result = addBoundedWhoisSetValue(nameservers, hostname.replace(/\.$/, ''), {
|
|
771
|
-
maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
|
|
772
|
-
field: 'nameservers', truncatedFields,
|
|
773
|
-
});
|
|
774
|
-
if (result === 'capped')
|
|
775
|
-
break;
|
|
776
|
-
if (result === 'added')
|
|
777
|
-
found += 1;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
if ((isDnsBelgium || isEurid) && !fields.registrar) {
|
|
782
|
-
const registrar = parseIndentedWhoisSubfield(text, /^[ \t]*Registrar[ \t]*:[ \t]*$/im, /^[ \t]*Name[ \t]*:[ \t]*(.+)$/i, whoisFieldLimit('registrar'));
|
|
783
|
-
if (registrar?.value)
|
|
784
|
-
fields.registrar = registrar.value;
|
|
785
|
-
if (registrar?.truncated)
|
|
786
|
-
truncatedFields.add('registrar');
|
|
787
|
-
}
|
|
788
|
-
if (isNorid) {
|
|
789
|
-
assignBoundedWhoisMatch(text, fields, 'registryDomainId', /^[ \t]*NORID Handle[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
|
|
790
|
-
assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Registrar Handle[ \t.]*:[ \t]*(.+)$/im, truncatedFields);
|
|
791
|
-
}
|
|
792
|
-
// These aliases are meaningful only inside their registry's
|
|
793
|
-
// distinctive response dialect. Keeping them marker-gated avoids
|
|
794
|
-
// treating generic contact IDs, validity text, or service-provider
|
|
795
|
-
// prose as domain-level evidence in unrelated WHOIS responses.
|
|
796
|
-
if (isCnnic) {
|
|
797
|
-
assignBoundedWhoisMatch(text, fields, 'registryDomainId', /^[ \t]*ROID[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
798
|
-
assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Expiration Time[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
799
|
-
}
|
|
800
|
-
if (isPandi) {
|
|
801
|
-
assignBoundedWhoisMatch(text, fields, 'registryDomainId', /^[ \t]*Domain ID[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
802
|
-
assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Sponsoring Registrar Organization[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
803
|
-
}
|
|
804
|
-
if (isIsocIl) {
|
|
805
|
-
assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*validity[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
806
|
-
}
|
|
807
|
-
if (isTwnic) {
|
|
808
|
-
assignBoundedWhoisMatch(text, fields, 'registrar', /^[ \t]*Registration Service Provider[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
809
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Record created on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
810
|
-
assignBoundedWhoisMatch(text, fields, 'expiryDate', /^[ \t]*Record expires on[ \t]*:[ \t]*(.+)$/im, truncatedFields);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
// Sectioned legacy port-43 responses use a distinctive pair of section
|
|
814
|
-
// headers and place several values on following indented lines. Gate the
|
|
815
|
-
// dialect on both markers so generic responses with one blank field are
|
|
816
|
-
// not reinterpreted. This is parser compatibility only: IANA still
|
|
817
|
-
// selects the endpoint and authority analysis remains unchanged.
|
|
818
|
-
const isSectionedRegistryResponse = !isRootHop
|
|
819
|
-
&& /^[ \t]*Relevant dates[ \t]*:[ \t]*$/im.test(text)
|
|
820
|
-
&& /^[ \t]*Registration status[ \t]*:[ \t]*$/im.test(text);
|
|
821
|
-
if (isSectionedRegistryResponse) {
|
|
822
|
-
for (const [key, headerRe] of [
|
|
823
|
-
['domainName', /^[ \t]*Domain(?: name)?[ \t]*:[ \t]*$/im],
|
|
824
|
-
['registrantName', /^[ \t]*Registrant[ \t]*:[ \t]*$/im],
|
|
825
|
-
['registrar', /^[ \t]*Registrar[ \t]*:[ \t]*$/im],
|
|
826
|
-
]) {
|
|
827
|
-
if (fields[key])
|
|
828
|
-
continue;
|
|
829
|
-
const parsed = parseIndentedWhoisValue(text, headerRe, whoisFieldLimit(key));
|
|
830
|
-
if (!parsed?.value)
|
|
831
|
-
continue;
|
|
832
|
-
fields[key] = parsed.value;
|
|
833
|
-
if (parsed.truncated)
|
|
834
|
-
truncatedFields.add(key);
|
|
835
|
-
}
|
|
836
|
-
const registrationStatus = parseIndentedWhoisValue(text, /^[ \t]*Registration status[ \t]*:[ \t]*$/im, 160);
|
|
837
|
-
if (registrationStatus?.value) {
|
|
838
|
-
addBoundedWhoisSetValue(statuses, registrationStatus.value, {
|
|
839
|
-
maxEntries: MAX_WHOIS_STATUSES,
|
|
840
|
-
maxLength: 160,
|
|
841
|
-
field: 'statuses',
|
|
842
|
-
truncatedFields,
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
const isChannelIslandsResponse = /^[ \t]*Domain Status[ \t]*:[ \t]*$/im.test(text)
|
|
846
|
-
&& /^[ \t]*WHOIS lookup made on[ \t]+[^\r\n]+$/im.test(text);
|
|
847
|
-
if (isChannelIslandsResponse) {
|
|
848
|
-
assignBoundedWhoisMatch(text, fields, 'createdDate', /^[ \t]*Registered on[ \t]+(.+)$/im, truncatedFields);
|
|
849
|
-
const domainStatus = parseIndentedWhoisValue(text, /^[ \t]*Domain Status[ \t]*:[ \t]*$/im, 160);
|
|
850
|
-
if (domainStatus?.value) {
|
|
851
|
-
addBoundedWhoisSetValue(statuses, domainStatus.value, {
|
|
852
|
-
maxEntries: MAX_WHOIS_STATUSES,
|
|
853
|
-
maxLength: 160,
|
|
854
|
-
field: 'statuses',
|
|
855
|
-
truncatedFields,
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (!isRootHop) {
|
|
861
|
-
for (const [prefix, label] of [
|
|
862
|
-
['registrant', 'Registrant'], ['admin', 'Admin(?:istrative)?'],
|
|
863
|
-
['tech', 'Tech(?:nical)?'], ['billing', 'Billing'],
|
|
864
|
-
]) {
|
|
865
|
-
const key = `${prefix}Street`;
|
|
866
|
-
if (expandedStreetFields.has(key))
|
|
867
|
-
continue;
|
|
868
|
-
const streetRe = new RegExp(`^[ \\t*]*${label} (?:Contact )?Street[ \\t.]*:[ \\t]*(.+)$`, 'gim');
|
|
869
|
-
const lines = [];
|
|
870
|
-
for (const match of text.matchAll(streetRe)) {
|
|
871
|
-
const bounded = boundedWhoisValue(match[1], 300);
|
|
872
|
-
if (bounded.truncated)
|
|
873
|
-
truncatedFields.add(key);
|
|
874
|
-
if (!bounded.value || lines.includes(bounded.value))
|
|
875
|
-
continue;
|
|
876
|
-
if (lines.length >= 4) {
|
|
877
|
-
truncatedFields.add(key);
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
lines.push(bounded.value);
|
|
881
|
-
}
|
|
882
|
-
if (lines.length)
|
|
883
|
-
fields[key] = lines.join(', ');
|
|
884
|
-
expandedStreetFields.add(key);
|
|
885
|
-
}
|
|
886
|
-
// The documented .nz protocol numbers up to two address lines using
|
|
887
|
-
// underscore field names. Aggregate only those exact fields, keeping
|
|
888
|
-
// the same four-line/300-character bounds as repeated ICANN-style
|
|
889
|
-
// street fields and leaving city/province/postcode separately typed.
|
|
890
|
-
for (const [prefix, nzPrefix] of [
|
|
891
|
-
['registrant', 'registrant'], ['admin', 'admin'], ['tech', 'technical'],
|
|
892
|
-
]) {
|
|
893
|
-
const key = `${prefix}Street`;
|
|
894
|
-
if (fields[key])
|
|
895
|
-
continue;
|
|
896
|
-
const addressRe = new RegExp(`^[ \\t]*${nzPrefix}_contact_address(?:1|2)[ \\t]*:[ \\t]*(.+)$`, 'gim');
|
|
897
|
-
const lines = [];
|
|
898
|
-
for (const match of text.matchAll(addressRe)) {
|
|
899
|
-
const bounded = boundedWhoisValue(match[1], 300);
|
|
900
|
-
if (bounded.truncated)
|
|
901
|
-
truncatedFields.add(key);
|
|
902
|
-
if (!bounded.value || lines.includes(bounded.value))
|
|
903
|
-
continue;
|
|
904
|
-
if (lines.length >= 4) {
|
|
905
|
-
truncatedFields.add(key);
|
|
906
|
-
break;
|
|
907
|
-
}
|
|
908
|
-
lines.push(bounded.value);
|
|
909
|
-
}
|
|
910
|
-
if (lines.length)
|
|
911
|
-
fields[key] = lines.join(', ');
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// If registrantName looks like it's actually a handle (a matching
|
|
915
|
-
// "contact: <handle>" block exists in this same hop), resolve it for
|
|
916
|
-
// the real name/org/email/phone/address. Harmless no-op otherwise -
|
|
917
|
-
// registries where "Registrant:" is already the real name (e.g. .kr)
|
|
918
|
-
// simply won't have a matching contact block to find.
|
|
919
|
-
if (!isRootHop && fields.registrantName) {
|
|
920
|
-
const resolved = resolveFredContact(text, fields.registrantName);
|
|
921
|
-
if (resolved) {
|
|
922
|
-
if (resolved.name)
|
|
923
|
-
fields.registrantName = resolved.name;
|
|
924
|
-
if (resolved.org && !fields.registrantOrg)
|
|
925
|
-
fields.registrantOrg = resolved.org;
|
|
926
|
-
if (resolved.email && !fields.registrantEmail)
|
|
927
|
-
fields.registrantEmail = resolved.email;
|
|
928
|
-
if (resolved.phone && !fields.registrantPhone)
|
|
929
|
-
fields.registrantPhone = resolved.phone;
|
|
930
|
-
if (resolved.address && !fields.registrantAddress)
|
|
931
|
-
fields.registrantAddress = resolved.address;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
// EDUCAUSE (.edu) and similar legacy registries list registrant/admin/
|
|
935
|
-
// technical contacts as indented blocks rather than "Field: value"
|
|
936
|
-
// pairs - see parseIndentedContactBlock. The plain "Registrant:" header
|
|
937
|
-
// has no separate person name on .edu (registrants are institutions),
|
|
938
|
-
// so its block's first line maps to the org, not a name.
|
|
939
|
-
if (!isRootHop && !fields.registrantOrg && !fields.registrantName) {
|
|
940
|
-
const block = parseIndentedContactBlock(text, /^[ \t]*Registrant:[ \t]*$/im);
|
|
941
|
-
if (block) {
|
|
942
|
-
if (block.name)
|
|
943
|
-
fields.registrantOrg = block.name;
|
|
944
|
-
if (block.address)
|
|
945
|
-
fields.registrantAddress = block.address;
|
|
946
|
-
if (block.phone)
|
|
947
|
-
fields.registrantPhone = block.phone;
|
|
948
|
-
if (block.email)
|
|
949
|
-
fields.registrantEmail = block.email;
|
|
950
|
-
if (block.truncated)
|
|
951
|
-
truncatedFields.add('registrantAddress');
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
if (!isRootHop && !fields.adminName) {
|
|
955
|
-
const block = parseIndentedContactBlock(text, /^[ \t]*Administrative Contact:[ \t]*$/im);
|
|
956
|
-
if (block) {
|
|
957
|
-
if (block.name)
|
|
958
|
-
fields.adminName = block.name;
|
|
959
|
-
if (block.address)
|
|
960
|
-
fields.adminAddress = block.address;
|
|
961
|
-
if (block.phone)
|
|
962
|
-
fields.adminPhone = block.phone;
|
|
963
|
-
if (block.email)
|
|
964
|
-
fields.adminEmail = block.email;
|
|
965
|
-
if (block.truncated)
|
|
966
|
-
truncatedFields.add('adminAddress');
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
if (!isRootHop && !fields.techName) {
|
|
970
|
-
const block = parseIndentedContactBlock(text, /^[ \t]*Technical Contact:[ \t]*$/im);
|
|
971
|
-
if (block) {
|
|
972
|
-
if (block.name)
|
|
973
|
-
fields.techName = block.name;
|
|
974
|
-
if (block.address)
|
|
975
|
-
fields.techAddress = block.address;
|
|
976
|
-
if (block.phone)
|
|
977
|
-
fields.techPhone = block.phone;
|
|
978
|
-
if (block.email)
|
|
979
|
-
fields.techEmail = block.email;
|
|
980
|
-
if (block.truncated)
|
|
981
|
-
truncatedFields.add('techAddress');
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
if (!isRootHop && !fields.billingName) {
|
|
985
|
-
const block = parseIndentedContactBlock(text, /^[ \t]*Billing Contact:[ \t]*$/im);
|
|
986
|
-
if (block) {
|
|
987
|
-
if (block.name)
|
|
988
|
-
fields.billingName = block.name;
|
|
989
|
-
if (block.address)
|
|
990
|
-
fields.billingAddress = block.address;
|
|
991
|
-
if (block.phone)
|
|
992
|
-
fields.billingPhone = block.phone;
|
|
993
|
-
if (block.email)
|
|
994
|
-
fields.billingEmail = block.email;
|
|
995
|
-
if (block.truncated)
|
|
996
|
-
truncatedFields.add('billingAddress');
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
// .jp (JPRS) uses a bracketed dual-language format instead of
|
|
1000
|
-
// "Label: value" - e.g. "[Domain Name] GOO.JP", with Japanese-only
|
|
1001
|
-
// labels alongside English ones for the same field. The bracket syntax
|
|
1002
|
-
// is distinctive enough to not need root-hop gating.
|
|
1003
|
-
if (!fields.domainName) {
|
|
1004
|
-
const m = text.match(/\[Domain Name\][ \t]*(.+)/i);
|
|
1005
|
-
if (m?.[1])
|
|
1006
|
-
fields.domainName = m[1].trim();
|
|
1007
|
-
}
|
|
1008
|
-
if (!fields.registrantName) {
|
|
1009
|
-
const m = text.match(/\[Registrant\][ \t]*(.+)/i);
|
|
1010
|
-
if (m?.[1])
|
|
1011
|
-
fields.registrantName = m[1].trim();
|
|
1012
|
-
}
|
|
1013
|
-
if (!fields.createdDate) {
|
|
1014
|
-
const m = text.match(/\[登録年月日\][ \t]*(.+)/);
|
|
1015
|
-
if (m?.[1])
|
|
1016
|
-
fields.createdDate = m[1].trim();
|
|
1017
|
-
}
|
|
1018
|
-
if (!fields.expiryDate) {
|
|
1019
|
-
const m = text.match(/\[有効期限\][ \t]*(.+)/);
|
|
1020
|
-
if (m?.[1])
|
|
1021
|
-
fields.expiryDate = m[1].trim();
|
|
1022
|
-
}
|
|
1023
|
-
for (const m of text.matchAll(/\[状態\][ \t]*(.+)/g)) {
|
|
1024
|
-
if (addBoundedWhoisSetValue(statuses, m[1], {
|
|
1025
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
1026
|
-
field: 'statuses', truncatedFields,
|
|
1027
|
-
}) === 'capped')
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
for (const m of text.matchAll(/\[Name Server\][ \t]*([a-zA-Z0-9.\-]+)/gi)) {
|
|
1031
|
-
if (addBoundedWhoisSetValue(nameservers, m[1], {
|
|
1032
|
-
maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
|
|
1033
|
-
field: 'nameservers', truncatedFields,
|
|
1034
|
-
}) === 'capped')
|
|
1035
|
-
break;
|
|
1036
|
-
}
|
|
1037
|
-
// "Name Server:" never collides with the IANA root hop (which uses
|
|
1038
|
-
// lowercase "nserver:" for the TLD's own root nameservers) so it's safe
|
|
1039
|
-
// on every hop; "nserver:"/"Host Name:"/"DNS:" are real per-domain
|
|
1040
|
-
// labels on some registries (e.g. .ru, .kr, .mx) but only once we're
|
|
1041
|
-
// past the root hop, for the same reason as the field patterns above.
|
|
1042
|
-
const nsLinePatterns = [/^[ \t*]*Name Server[ \t.]*:[ \t]*([a-zA-Z0-9.\-]+)/gim];
|
|
1043
|
-
if (!isRootHop) {
|
|
1044
|
-
nsLinePatterns.push(/^[ \t*]*nserver[ \t.]*:[ \t]*([a-zA-Z0-9.\-]+)/gim, /^[ \t*]*Nameserver[ \t.]*:[ \t]*([a-zA-Z0-9.\-]+)/gim, /^[ \t*]*Host Name[ \t.]*:[ \t]*([a-zA-Z0-9.\-]+)/gim, /^[ \t]*ns_name_\d{2}[ \t]*:[ \t]*([a-zA-Z0-9.\-]+)/gim);
|
|
1045
|
-
// Punktum dk uses `DNS: example.dk` for the queried domain, then
|
|
1046
|
-
// `Hostname:` inside its nameserver section. Other supported
|
|
1047
|
-
// registries use `DNS:` for an actual nameserver, so switch aliases
|
|
1048
|
-
// only when the full .dk marker set is present.
|
|
1049
|
-
nsLinePatterns.push(isPunktum
|
|
1050
|
-
? /^[ \t*]*Hostname[ \t.]*:[ \t]*([a-zA-Z0-9.\-]+)/gim
|
|
1051
|
-
: /^[ \t*]*DNS[ \t.]*:[ \t]*([a-zA-Z0-9.\-]+)/gim);
|
|
1052
|
-
}
|
|
1053
|
-
for (const re of nsLinePatterns) {
|
|
1054
|
-
for (const m of text.matchAll(re)) {
|
|
1055
|
-
if (addBoundedWhoisSetValue(nameservers, m[1], {
|
|
1056
|
-
maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
|
|
1057
|
-
field: 'nameservers', truncatedFields,
|
|
1058
|
-
}) === 'capped')
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
// Same root-hop exclusion as above: bare "status:" is also how IANA
|
|
1063
|
-
// reports the TLD's own delegation status (e.g. "status: ACTIVE" for
|
|
1064
|
-
// .gt itself), not the queried domain's status. Deliberately not adding
|
|
1065
|
-
// "state:" as an alternate here (some registries, e.g. .ru/.se, use it
|
|
1066
|
-
// for domain status) - it's also the standard label for a postal
|
|
1067
|
-
// address's state/province in Name/City/State/Country contact blocks
|
|
1068
|
-
// (seen on .mx), and there's no reliable way to tell those apart from
|
|
1069
|
-
// the label alone - a missing status is safer than a wrong one.
|
|
1070
|
-
const statusRe = isRootHop
|
|
1071
|
-
? /^[ \t*]*Domain Status[ \t.]*:[ \t]*([a-zA-Z][a-zA-Z0-9_-]*)/gim
|
|
1072
|
-
: (isDnsBelgium || isPunktum || isIsocIl || isTwnic || isEif
|
|
1073
|
-
|| isHkirc || isNicKz || isNicMd || isAtiTn)
|
|
1074
|
-
? /^[ \t*]*(?:Domain Status|Status)[ \t.]*:[ \t]*([^\r\n]+)/gim
|
|
1075
|
-
: /^[ \t*]*(?:Domain Status|Status)[ \t.]*:[ \t]*([a-zA-Z][a-zA-Z0-9_-]*)/gim;
|
|
1076
|
-
if (!isRnids) {
|
|
1077
|
-
for (const m of text.matchAll(statusRe)) {
|
|
1078
|
-
if (addBoundedWhoisSetValue(statuses, m[1], {
|
|
1079
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
1080
|
-
field: 'statuses', truncatedFields,
|
|
1081
|
-
}) === 'capped')
|
|
1082
|
-
break;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
if (!isRootHop) {
|
|
1086
|
-
for (const m of text.matchAll(/^[ \t]*query_status[ \t]*:[ \t]*(\d{3}(?:[ \t]+[^\r\n]+)?)/gim)) {
|
|
1087
|
-
if (addBoundedWhoisSetValue(statuses, m[1], {
|
|
1088
|
-
maxEntries: MAX_WHOIS_STATUSES, maxLength: 160,
|
|
1089
|
-
field: 'statuses', truncatedFields,
|
|
1090
|
-
}) === 'capped')
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
// Some registries (e.g. .it, .tr) list nameservers as a bare header
|
|
1095
|
-
// ("Nameservers", "Domain Servers") followed by unlabeled lines -
|
|
1096
|
-
// sometimes just a hostname, sometimes "hostname ip.addr" - rather
|
|
1097
|
-
// than a per-line "Name Server:" label. Only meaningful on non-root hops.
|
|
1098
|
-
if (!isRootHop && nameservers.size === 0) {
|
|
1099
|
-
const headerMatch = text.match(/^[ \t*]*(?:Name ?[Ss]ervers|Domain Servers|DNS servers)[ \t.]*:?[ \t]*$/mi);
|
|
1100
|
-
if (headerMatch) {
|
|
1101
|
-
let found = 0;
|
|
1102
|
-
for (const line of text.slice((headerMatch.index ?? 0) + headerMatch[0].length).split('\n')) {
|
|
1103
|
-
const trimmed = line.trim();
|
|
1104
|
-
if (!trimmed) {
|
|
1105
|
-
if (found > 0)
|
|
1106
|
-
break; // blank line after >=1 hostname ends the section
|
|
1107
|
-
continue; // the header line's own line break - not a real gap yet
|
|
1108
|
-
}
|
|
1109
|
-
const hostMatch = trimmed.match(/^([a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})(?:\s|$)/);
|
|
1110
|
-
if (hostMatch) {
|
|
1111
|
-
const result = addBoundedWhoisSetValue(nameservers, hostMatch[1], {
|
|
1112
|
-
maxEntries: MAX_WHOIS_NAMESERVERS, maxLength: 253,
|
|
1113
|
-
field: 'nameservers', truncatedFields,
|
|
1114
|
-
});
|
|
1115
|
-
if (result === 'capped')
|
|
1116
|
-
break;
|
|
1117
|
-
if (result === 'added')
|
|
1118
|
-
found += 1;
|
|
1119
|
-
}
|
|
1120
|
-
else {
|
|
1121
|
-
break;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
// Dedicated legacy-format parsers above populate some fields outside the
|
|
1128
|
-
// generic pattern loop. Apply the same string/control bounds to every
|
|
1129
|
-
// scalar before constructing compatibility and normalized contact views.
|
|
1130
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
1131
|
-
if (typeof value !== 'string')
|
|
1132
|
-
continue;
|
|
1133
|
-
const bounded = boundedWhoisValue(value, whoisFieldLimit(key));
|
|
1134
|
-
if (!bounded.value)
|
|
1135
|
-
delete fields[key];
|
|
1136
|
-
else
|
|
1137
|
-
fields[key] = bounded.value;
|
|
1138
|
-
if (bounded.truncated)
|
|
1139
|
-
truncatedFields.add(key);
|
|
1140
|
-
}
|
|
1141
|
-
const createdDateIso = registryDateIso(fields.createdDate);
|
|
1142
|
-
const expiryDateIso = registryDateIso(fields.expiryDate);
|
|
1143
|
-
const updatedDateIso = registryDateIso(fields.updatedDate);
|
|
1144
|
-
const lifecycle = {
|
|
1145
|
-
createdDate: fields.createdDate || null,
|
|
1146
|
-
expiryDate: fields.expiryDate || null,
|
|
1147
|
-
updatedDate: fields.updatedDate || null,
|
|
1148
|
-
createdDateIso,
|
|
1149
|
-
expiryDateIso,
|
|
1150
|
-
updatedDateIso,
|
|
1151
|
-
};
|
|
1152
|
-
const contactsByRole = {};
|
|
1153
|
-
for (const [prefix, role] of [
|
|
1154
|
-
['registrant', 'registrant'], ['admin', 'administrative'],
|
|
1155
|
-
['tech', 'technical'], ['billing', 'billing'],
|
|
1156
|
-
]) {
|
|
1157
|
-
const contact = normalizedWhoisContact(fields, prefix, role, truncatedFields);
|
|
1158
|
-
if (contact)
|
|
1159
|
-
contactsByRole[role] = [contact];
|
|
1160
|
-
}
|
|
1161
|
-
if (fields.abuseEmail || fields.abusePhone) {
|
|
1162
|
-
contactsByRole.abuse = [{
|
|
1163
|
-
handle: null, roles: ['abuse'], name: null, names: [], org: fields.registrar || null,
|
|
1164
|
-
organizations: fields.registrar ? [fields.registrar] : [],
|
|
1165
|
-
email: fields.abuseEmail || null, emails: fields.abuseEmail ? [fields.abuseEmail] : [],
|
|
1166
|
-
phone: fields.abusePhone || null, phones: fields.abusePhone ? [fields.abusePhone] : [],
|
|
1167
|
-
address: null, addresses: [], publicIds: [], links: [],
|
|
1168
|
-
}];
|
|
1169
|
-
}
|
|
1170
|
-
// Existence is decided authority-aware, not by a global "any hop said no
|
|
1171
|
-
// match" flag: positive registry evidence is never overridden by a later
|
|
1172
|
-
// registrar hop that failed, rate-limited, or returned "no match".
|
|
1173
|
-
const authority = analyzeWhoisChainAuthority(source);
|
|
1174
|
-
return {
|
|
1175
|
-
...fields,
|
|
1176
|
-
nameservers: [...nameservers],
|
|
1177
|
-
statuses: [...statuses],
|
|
1178
|
-
createdDateIso,
|
|
1179
|
-
expiryDateIso,
|
|
1180
|
-
updatedDateIso,
|
|
1181
|
-
lifecycle,
|
|
1182
|
-
contactsByRole,
|
|
1183
|
-
fieldsTruncated: [...truncatedFields].sort(),
|
|
1184
|
-
...authority,
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
export { buildWhoisChain, parseWhoisChain, analyzeWhoisChainAuthority, whoisQuery, queryWhoisAddress, buildWhoisChainUncached, };
|
|
1
|
+
// WHOIS: stable transport, referral-chain, authority and parsing facade shared
|
|
2
|
+
// by the Express server, Netlify Functions and CLI.
|
|
3
|
+
export { queryWhoisAddress, whoisQuery, } from "./whois-transport.mjs";
|
|
4
|
+
export { buildWhoisChain, buildWhoisChainUncached, } from "./whois-chain.mjs";
|
|
5
|
+
export { analyzeWhoisChainAuthority } from "./whois-authority.mjs";
|
|
6
|
+
export { parseWhoisChain } from "./whois-parser.mjs";
|