@slicedearth/whoisleuth-cli 1.32.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/LICENSE +661 -0
- package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
- package/NOTICE +76 -0
- package/README.md +84 -0
- package/TRADEMARKS.md +41 -0
- package/bin/whoisleuth.mjs +35 -0
- package/cli/archive-inspect.mjs +198 -0
- package/cli/arguments.mjs +1022 -0
- package/cli/artifact-verify.mjs +210 -0
- package/cli/bulk-checkpoint.mjs +218 -0
- package/cli/bulk-command-runner.mjs +109 -0
- package/cli/bulk-output.mjs +112 -0
- package/cli/bulk.mjs +133 -0
- package/cli/compare.mjs +238 -0
- package/cli/completion.mjs +175 -0
- package/cli/csv.mjs +1 -0
- package/cli/discover.mjs +50 -0
- package/cli/discovery-command-runner.mjs +36 -0
- package/cli/discovery-observation-snapshot.mjs +315 -0
- package/cli/discovery-scan-command-runner.mjs +141 -0
- package/cli/discovery-scan.mjs +325 -0
- package/cli/discovery-snapshot.mjs +133 -0
- package/cli/discovery-workflow.mjs +73 -0
- package/cli/doctor.mjs +137 -0
- package/cli/errors.mjs +21 -0
- package/cli/evidence-command-arguments.mjs +145 -0
- package/cli/evidence-command-runner.mjs +106 -0
- package/cli/evidence-signing.mjs +215 -0
- package/cli/exit-codes.mjs +9 -0
- package/cli/export-evidence.mjs +28 -0
- package/cli/formatters/evidence-report.mjs +3 -0
- package/cli/formatters/html.mjs +69 -0
- package/cli/formatters/json.mjs +135 -0
- package/cli/formatters/markdown.mjs +3 -0
- package/cli/formatters/terminal.mjs +658 -0
- package/cli/http.mjs +27 -0
- package/cli/lookup-command-runner.mjs +87 -0
- package/cli/lookup-diff.mjs +171 -0
- package/cli/mail-review.mjs +204 -0
- package/cli/manual.mjs +34 -0
- package/cli/network-command-runner.mjs +88 -0
- package/cli/output-file.mjs +97 -0
- package/cli/page-compare.mjs +123 -0
- package/cli/posture.mjs +20 -0
- package/cli/progress-events.mjs +53 -0
- package/cli/progress.mjs +56 -0
- package/cli/registry-access.mjs +3 -0
- package/cli/registry-support.mjs +120 -0
- package/cli/risk-calibration.mjs +294 -0
- package/cli/runner-types.mjs +1 -0
- package/cli/runner.mjs +726 -0
- package/cli/saved-lookup.mjs +87 -0
- package/cli/source-reliability.mjs +727 -0
- package/cli/strict-exit.mjs +55 -0
- package/cli/terminal-presentation.mjs +147 -0
- package/docs/cli.md +827 -0
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
- package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
- package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
- package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
- package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
- package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
- package/frontend/src/lib/analysis/bulk-sort.js +49 -0
- package/frontend/src/lib/analysis/campaign-model.js +265 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
- package/frontend/src/lib/analysis/case-migration-model.js +248 -0
- package/frontend/src/lib/analysis/case-model.js +5 -0
- package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
- package/frontend/src/lib/analysis/case-record-core.js +232 -0
- package/frontend/src/lib/analysis/case-record-model.js +7 -0
- package/frontend/src/lib/analysis/case-record-operations.js +224 -0
- package/frontend/src/lib/analysis/case-response-model.js +640 -0
- package/frontend/src/lib/analysis/case-response-packet.js +573 -0
- package/frontend/src/lib/analysis/case-storage-model.js +93 -0
- package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
- package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
- package/frontend/src/lib/analysis/http-summary.js +165 -0
- package/frontend/src/lib/analysis/investigation-guide.js +500 -0
- package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
- package/frontend/src/lib/analysis/page-baseline.js +272 -0
- package/frontend/src/lib/analysis/page-similarity.js +179 -0
- package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
- package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
- package/frontend/src/lib/analysis/scoring.js +135 -0
- package/frontend/src/lib/analysis/shortlist-model.js +209 -0
- package/frontend/src/lib/analysis/utils.js +265 -0
- package/frontend/src/lib/analysis/watchlist-history.js +643 -0
- package/frontend/src/lib/analysis/watchlist-store.js +127 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
- package/frontend/src/lib/analysis/workspace-archive.js +472 -0
- package/lib/abort.mjs +26 -0
- package/lib/abusech-auth.mjs +19 -0
- package/lib/availability.mjs +716 -0
- package/lib/bounded-contract-normalizers.mjs +61 -0
- package/lib/browser-library-profile.mjs +310 -0
- package/lib/bulk-comparison-evidence.mjs +83 -0
- package/lib/classify.mjs +93 -0
- package/lib/client-behavior-profile.mjs +83 -0
- package/lib/credential-surface-profile.mjs +66 -0
- package/lib/csv.mjs +23 -0
- package/lib/ct-query.mjs +27 -0
- package/lib/ct-search.mjs +393 -0
- package/lib/distributed-operation-budget.mjs +461 -0
- package/lib/dns-delegation-health.mjs +344 -0
- package/lib/dns-intelligence.mjs +739 -0
- package/lib/dns-mx.mjs +24 -0
- package/lib/dns-resolver-selection.mjs +33 -0
- package/lib/domain-posture-analysis.mjs +254 -0
- package/lib/domain-posture-parsers.mjs +358 -0
- package/lib/domain-posture.mjs +745 -0
- package/lib/error-detail.mjs +10 -0
- package/lib/evidence-export.mjs +431 -0
- package/lib/evidence-report-markdown.mjs +71 -0
- package/lib/evidence-report.mjs +391 -0
- package/lib/external-intelligence-risk.mjs +108 -0
- package/lib/favicon.mjs +146 -0
- package/lib/feature-policy.mjs +70 -0
- package/lib/generated/retire-browser-catalog.mjs +8724 -0
- package/lib/generated/unicode-confusables-17.mjs +93 -0
- package/lib/hostname.mjs +38 -0
- package/lib/html-signals.mjs +680 -0
- package/lib/http-intelligence.mjs +232 -0
- package/lib/idn-confusable-policy.mjs +86 -0
- package/lib/idn-confusables.mjs +465 -0
- package/lib/lookup-cache.mjs +130 -0
- package/lib/lookup-progress.mjs +291 -0
- package/lib/lookup-source-progress.mjs +109 -0
- package/lib/lookup-threat-provider-inventory.mjs +13 -0
- package/lib/lookup.mjs +592 -0
- package/lib/observation.mjs +114 -0
- package/lib/observed-network-context.mjs +355 -0
- package/lib/operation-budget.mjs +489 -0
- package/lib/page-fingerprints.mjs +481 -0
- package/lib/page-role-profile.mjs +117 -0
- package/lib/perceptual-hash.mjs +451 -0
- package/lib/rdap-attempts.mjs +34 -0
- package/lib/rdap-bootstrap.mjs +173 -0
- package/lib/rdap-capabilities.mjs +292 -0
- package/lib/rdap-client.mjs +125 -0
- package/lib/rdap-normalization.mjs +691 -0
- package/lib/rdap-registrar.mjs +196 -0
- package/lib/rdap-transport.mjs +39 -0
- package/lib/rdap-types.mjs +1 -0
- package/lib/rdap-validation.mjs +94 -0
- package/lib/rdap.mjs +16 -0
- package/lib/registry-access.mjs +32 -0
- package/lib/registry-capabilities.mjs +117 -0
- package/lib/registry-capability-catalogue.mjs +1206 -0
- package/lib/registry-comparison.mjs +318 -0
- package/lib/registry-dates.mjs +190 -0
- package/lib/registry-insights.mjs +325 -0
- package/lib/response-policy.mjs +256 -0
- package/lib/risk-scoring.mjs +164 -0
- package/lib/safe-fetch.mjs +446 -0
- package/lib/security-txt.mjs +359 -0
- package/lib/service-binding-dns.mjs +608 -0
- package/lib/sslbl-certificates.generated.mjs +143 -0
- package/lib/sslbl-intelligence.mjs +241 -0
- package/lib/static-html-analysis.mjs +340 -0
- package/lib/structured-data-identity.mjs +323 -0
- package/lib/threat-intelligence-contract.mjs +5 -0
- package/lib/threat-intelligence-definition-registry.mjs +34 -0
- package/lib/threat-intelligence-fixtures.mjs +82 -0
- package/lib/threat-intelligence-manifests.mjs +512 -0
- package/lib/threat-intelligence-runtime.mjs +662 -0
- package/lib/threat-intelligence-targets.mjs +168 -0
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +307 -0
- package/lib/tls-intelligence.mjs +814 -0
- package/lib/typosquat-generator.mjs +884 -0
- package/lib/urlhaus-intelligence.mjs +361 -0
- package/lib/urlscan-intelligence.mjs +349 -0
- package/lib/website-security-posture.mjs +325 -0
- package/lib/website-technology.mjs +437 -0
- package/lib/whois-authority.mjs +98 -0
- package/lib/whois-chain.mjs +261 -0
- package/lib/whois-contacts.mjs +163 -0
- package/lib/whois-contracts.mjs +3 -0
- package/lib/whois-normalization.mjs +27 -0
- package/lib/whois-transport.mjs +100 -0
- package/lib/whois-values.mjs +92 -0
- package/lib/whois.mjs +1187 -0
- package/package.json +55 -0
- package/third-party-notices.txt +5571 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// WHOIS referral-chain orchestration. Transport remains isolated in
|
|
2
|
+
// whois-transport.mts; this module applies registry query profiles, follows
|
|
3
|
+
// bounded referrals, and adapts the registry-web fallback into WHOIS text.
|
|
4
|
+
import { domainToUnicode } from 'node:url';
|
|
5
|
+
import { cached } from "./lookup-cache.mjs";
|
|
6
|
+
import { registryCapabilityFor } from "./registry-capabilities.mjs";
|
|
7
|
+
import { readTextCapped, safeFetch } from "./safe-fetch.mjs";
|
|
8
|
+
import { whoisQuery } from "./whois-transport.mjs";
|
|
9
|
+
const IANA_WHOIS = 'whois.iana.org';
|
|
10
|
+
const WHOIS_HOP_DEADLINE_MS = 12_000;
|
|
11
|
+
const WHOIS_CHAIN_DEADLINE_MS = 25_000;
|
|
12
|
+
const MAX_GT_REGISTRY_HTML_BYTES = 500_000;
|
|
13
|
+
const WHOIS_QUERY_FORMATTERS = {
|
|
14
|
+
'plain-domain': (domain) => domain,
|
|
15
|
+
'denic-domain-ace': (domain) => `-T dn,ace ${domain}`,
|
|
16
|
+
'jprs-domain-english': (domain) => `${domain}/e`,
|
|
17
|
+
'registry-domain-unicode': (domain) => domainToUnicode(domain),
|
|
18
|
+
};
|
|
19
|
+
function whoisTransportForHop(domain, hop) {
|
|
20
|
+
const capability = registryCapabilityFor(domain);
|
|
21
|
+
const queryProfile = hop === 1 && capability?.whoisQueryScope === 'first-referral'
|
|
22
|
+
? capability.whoisQueryProfile
|
|
23
|
+
: 'plain-domain';
|
|
24
|
+
return {
|
|
25
|
+
query: WHOIS_QUERY_FORMATTERS[queryProfile](domain),
|
|
26
|
+
queryProfile,
|
|
27
|
+
responseEncoding: capability?.whoisEncodingProfile || 'utf-8',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function errorMessage(value, fallback) {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
return fallback;
|
|
33
|
+
const message = value.message;
|
|
34
|
+
return message ? String(message) : fallback;
|
|
35
|
+
}
|
|
36
|
+
function extractReferral(whoisText) {
|
|
37
|
+
// Horizontal whitespace is intentional. `\s*` can cross a blank line and
|
|
38
|
+
// misread the next field label when a registry publishes an empty referral.
|
|
39
|
+
const patterns = [
|
|
40
|
+
/^[ \t]*refer:[ \t]*([a-zA-Z0-9.\-]+)/mi,
|
|
41
|
+
/^[ \t]*ReferralServer:[ \t]*whois:\/\/([a-zA-Z0-9.\-]+)/mi,
|
|
42
|
+
/^[ \t]*whois:[ \t]*([a-zA-Z0-9.\-]+)/mi,
|
|
43
|
+
];
|
|
44
|
+
for (const pattern of patterns) {
|
|
45
|
+
const referral = whoisText.match(pattern)?.[1]?.trim();
|
|
46
|
+
if (referral)
|
|
47
|
+
return referral;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function stripTags(html) {
|
|
52
|
+
return html
|
|
53
|
+
.replace(/<[^>]+>/g, ' ')
|
|
54
|
+
.replace(/ /g, ' ')
|
|
55
|
+
.replace(/\s+/g, ' ')
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
function sectionBetween(html, startRe, endRes) {
|
|
59
|
+
const startMatch = html.match(startRe);
|
|
60
|
+
if (!startMatch)
|
|
61
|
+
return '';
|
|
62
|
+
const rest = html.slice((startMatch.index ?? 0) + startMatch[0].length);
|
|
63
|
+
let endIndex = rest.length;
|
|
64
|
+
for (const endRe of endRes) {
|
|
65
|
+
const match = rest.match(endRe);
|
|
66
|
+
if (!match)
|
|
67
|
+
continue;
|
|
68
|
+
const matchIndex = match.index ?? 0;
|
|
69
|
+
const index = match[0].startsWith('>') ? matchIndex + 1 : matchIndex;
|
|
70
|
+
if (index < endIndex)
|
|
71
|
+
endIndex = index;
|
|
72
|
+
}
|
|
73
|
+
return rest.slice(0, endIndex);
|
|
74
|
+
}
|
|
75
|
+
function extractIconFields(html, iconMap) {
|
|
76
|
+
let marked = html;
|
|
77
|
+
for (const [icon, key] of Object.entries(iconMap)) {
|
|
78
|
+
marked = marked.replace(new RegExp(`<i[^>]*\\b${icon}\\b[^>]*></i>`, 'gi'), `\n@@${key}@@\n`);
|
|
79
|
+
}
|
|
80
|
+
const parts = stripTags(marked).split(/@@(\w+)@@/);
|
|
81
|
+
const fields = {};
|
|
82
|
+
for (let index = 1; index < parts.length; index += 2) {
|
|
83
|
+
const value = (parts[index + 1] || '').trim();
|
|
84
|
+
const key = parts[index];
|
|
85
|
+
if (key && value && !fields[key])
|
|
86
|
+
fields[key] = value;
|
|
87
|
+
}
|
|
88
|
+
return fields;
|
|
89
|
+
}
|
|
90
|
+
export async function fetchGtRegistryWhois(domain, { fetcher = safeFetch, } = {}) {
|
|
91
|
+
const url = `https://www.gt/sitio/whois.php?dn=${encodeURIComponent(domain)}.&lang=en`;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timeout = setTimeout(() => controller.abort(), 8_000);
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetcher(url, {
|
|
96
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; DomainStatusChecker/1.0)' },
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
await response.body?.cancel().catch(() => { });
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const body = await readTextCapped(response, MAX_GT_REGISTRY_HTML_BYTES);
|
|
104
|
+
if (body.truncated)
|
|
105
|
+
return null;
|
|
106
|
+
const html = body.text;
|
|
107
|
+
if (/is not registered/i.test(html))
|
|
108
|
+
return { registered: false };
|
|
109
|
+
const statusMatch = html.match(/<i class="fas fa-bell fa-fw"><\/i>\s*([A-Za-z]+)/i);
|
|
110
|
+
const expiryMatch = html.match(/Expiration:\s*([0-9]{4}-[A-Za-z]{3}-[0-9]{2}[^<]*)/i);
|
|
111
|
+
const orgSection = sectionBetween(html, /Entitled Organization/i, [/Servers\s*<\/h4>/i]);
|
|
112
|
+
const org = extractIconFields(orgSection, {
|
|
113
|
+
'fa-building': 'org',
|
|
114
|
+
'fa-address-card': 'address',
|
|
115
|
+
'fa-phone': 'phone',
|
|
116
|
+
});
|
|
117
|
+
const adminSection = sectionBetween(html, />\s*ADMINISTRATIVE\s*</i, [/>\s*TECHNICAL\s*</i, />\s*BILLING\s*</i]);
|
|
118
|
+
const admin = extractIconFields(adminSection, {
|
|
119
|
+
'fa-user': 'name',
|
|
120
|
+
'fa-envelope': 'email',
|
|
121
|
+
'fa-address-card': 'address',
|
|
122
|
+
'fa-building': 'org',
|
|
123
|
+
});
|
|
124
|
+
const serversSection = sectionBetween(html, /Servers\s*<\/h4>/i, [
|
|
125
|
+
/<div class="span6">/i,
|
|
126
|
+
]);
|
|
127
|
+
const nameservers = [
|
|
128
|
+
...serversSection.matchAll(/<strong>\s*([a-zA-Z0-9.\-]+)\.?\s*<\/strong>/gi),
|
|
129
|
+
]
|
|
130
|
+
.map((match) => match[1]?.trim())
|
|
131
|
+
.filter((value) => Boolean(value));
|
|
132
|
+
return {
|
|
133
|
+
registered: true,
|
|
134
|
+
status: statusMatch?.[1]?.trim() || null,
|
|
135
|
+
expiryDate: expiryMatch?.[1]?.trim() || null,
|
|
136
|
+
registrantOrg: org.org || null,
|
|
137
|
+
registrantAddress: org.address || null,
|
|
138
|
+
registrantPhone: org.phone || null,
|
|
139
|
+
adminName: admin.name || null,
|
|
140
|
+
adminOrg: admin.org || null,
|
|
141
|
+
adminEmail: admin.email || null,
|
|
142
|
+
nameservers,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function formatGtResultAsText(domain, result) {
|
|
153
|
+
if (!result.registered)
|
|
154
|
+
return `No match for domain ${domain.toUpperCase()}.`;
|
|
155
|
+
const lines = [`Domain Name: ${domain.toUpperCase()}`];
|
|
156
|
+
if (result.status)
|
|
157
|
+
lines.push(`Domain Status: ${result.status}`);
|
|
158
|
+
if (result.expiryDate)
|
|
159
|
+
lines.push(`Registry Expiry Date: ${result.expiryDate}`);
|
|
160
|
+
if (result.registrantOrg)
|
|
161
|
+
lines.push(`Registrant Organization: ${result.registrantOrg}`);
|
|
162
|
+
if (result.registrantAddress)
|
|
163
|
+
lines.push(`Registrant Address: ${result.registrantAddress}`);
|
|
164
|
+
if (result.registrantPhone)
|
|
165
|
+
lines.push(`Registrant Phone: ${result.registrantPhone}`);
|
|
166
|
+
if (result.adminName)
|
|
167
|
+
lines.push(`Admin Name: ${result.adminName}`);
|
|
168
|
+
if (result.adminOrg)
|
|
169
|
+
lines.push(`Admin Organization: ${result.adminOrg}`);
|
|
170
|
+
if (result.adminEmail)
|
|
171
|
+
lines.push(`Admin Email: ${result.adminEmail}`);
|
|
172
|
+
for (const nameserver of result.nameservers)
|
|
173
|
+
lines.push(`Name Server: ${nameserver}`);
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
export async function buildWhoisChainUncached(query, options = {}) {
|
|
177
|
+
const queryWhois = options.whoisQuery || whoisQuery;
|
|
178
|
+
const fetchGtWhois = options.fetchGtRegistryWhois || fetchGtRegistryWhois;
|
|
179
|
+
const now = options.now || Date.now;
|
|
180
|
+
const chainDeadlineMs = options.chainDeadlineMs || WHOIS_CHAIN_DEADLINE_MS;
|
|
181
|
+
const chain = [];
|
|
182
|
+
const visited = new Set();
|
|
183
|
+
let currentServer = IANA_WHOIS;
|
|
184
|
+
const startedAt = now();
|
|
185
|
+
for (let hop = 0; hop < 6; hop += 1) {
|
|
186
|
+
if (visited.has(currentServer.toLowerCase()))
|
|
187
|
+
break;
|
|
188
|
+
visited.add(currentServer.toLowerCase());
|
|
189
|
+
const transport = whoisTransportForHop(query, hop);
|
|
190
|
+
const remainingMs = chainDeadlineMs - (now() - startedAt);
|
|
191
|
+
if (remainingMs <= 0) {
|
|
192
|
+
chain.push({
|
|
193
|
+
server: currentServer,
|
|
194
|
+
queriedAt: new Date().toISOString(),
|
|
195
|
+
queryProfile: transport.queryProfile,
|
|
196
|
+
responseEncoding: transport.responseEncoding,
|
|
197
|
+
error: 'WHOIS referral chain exceeded the total time limit',
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
let responseText;
|
|
202
|
+
let address = null;
|
|
203
|
+
const queriedAt = new Date().toISOString();
|
|
204
|
+
try {
|
|
205
|
+
responseText = await queryWhois(currentServer, transport.query, {
|
|
206
|
+
timeoutMs: Math.min(10_000, remainingMs),
|
|
207
|
+
totalDeadlineMs: Math.min(WHOIS_HOP_DEADLINE_MS, remainingMs),
|
|
208
|
+
onAddressSelected: (selected) => {
|
|
209
|
+
address = selected;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (cause) {
|
|
214
|
+
chain.push({
|
|
215
|
+
server: currentServer,
|
|
216
|
+
queriedAt,
|
|
217
|
+
queryProfile: transport.queryProfile,
|
|
218
|
+
responseEncoding: transport.responseEncoding,
|
|
219
|
+
error: errorMessage(cause, 'WHOIS request failed'),
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
chain.push({
|
|
224
|
+
server: currentServer,
|
|
225
|
+
address,
|
|
226
|
+
queriedAt,
|
|
227
|
+
queryProfile: transport.queryProfile,
|
|
228
|
+
responseEncoding: transport.responseEncoding,
|
|
229
|
+
response: responseText,
|
|
230
|
+
});
|
|
231
|
+
const referral = extractReferral(responseText);
|
|
232
|
+
if (!referral || referral.toLowerCase() === currentServer.toLowerCase())
|
|
233
|
+
break;
|
|
234
|
+
currentServer = referral;
|
|
235
|
+
}
|
|
236
|
+
const firstHop = chain[0];
|
|
237
|
+
if (query.toLowerCase().endsWith('.gt') &&
|
|
238
|
+
chain.length === 1 &&
|
|
239
|
+
firstHop &&
|
|
240
|
+
!('error' in firstHop)) {
|
|
241
|
+
try {
|
|
242
|
+
const result = await fetchGtWhois(query);
|
|
243
|
+
if (result) {
|
|
244
|
+
chain.push({
|
|
245
|
+
server: 'www.gt (registry website - .gt has no WHOIS:43 server)',
|
|
246
|
+
queriedAt: new Date().toISOString(),
|
|
247
|
+
queryProfile: 'gt-registry-web',
|
|
248
|
+
responseEncoding: 'utf-8',
|
|
249
|
+
response: formatGtResultAsText(query, result),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Best-effort registry fallback. The IANA hop remains explicit on failure.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return chain;
|
|
258
|
+
}
|
|
259
|
+
export async function buildWhoisChain(query) {
|
|
260
|
+
return cached(`whois:${query.toLowerCase()}`, () => buildWhoisChainUncached(query));
|
|
261
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Registry-specific contact block resolvers. These helpers only interpret
|
|
2
|
+
// formats after their exact marker or handle relationship has been proven.
|
|
3
|
+
import { boundedWhoisValue } from "./whois-values.mjs";
|
|
4
|
+
export function resolveFredContact(text, handle) {
|
|
5
|
+
if (!handle)
|
|
6
|
+
return null;
|
|
7
|
+
const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
const headerMatch = text.match(new RegExp(`^[ \\t]*contact:[ \\t]*${escaped}[ \\t]*$`, 'im'));
|
|
9
|
+
if (!headerMatch)
|
|
10
|
+
return null;
|
|
11
|
+
const rest = text.slice((headerMatch.index ?? 0) + headerMatch[0].length);
|
|
12
|
+
const endMatch = rest.match(/\n[ \t]*\n|^[ \t]*(?:domain|nsset|contact):/im);
|
|
13
|
+
const block = endMatch ? rest.slice(0, endMatch.index) : rest;
|
|
14
|
+
const get = (pattern) => {
|
|
15
|
+
const match = block.match(pattern);
|
|
16
|
+
return match?.[1]?.trim() || null;
|
|
17
|
+
};
|
|
18
|
+
const addresses = [...block.matchAll(/^[ \t]*address:[ \t]*(.+)$/gim)]
|
|
19
|
+
.map((match) => match[1]?.trim())
|
|
20
|
+
.filter((value) => Boolean(value));
|
|
21
|
+
return {
|
|
22
|
+
name: get(/^[ \t]*name:[ \t]*(.+)$/im),
|
|
23
|
+
org: get(/^[ \t]*org:[ \t]*(.+)$/im),
|
|
24
|
+
email: get(/^[ \t]*e-?mail:[ \t]*(.+)$/im),
|
|
25
|
+
phone: get(/^[ \t]*phone:[ \t]*(.+)$/im),
|
|
26
|
+
address: addresses.length ? addresses.join(', ') : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function resolveIsnicRole(text, handle) {
|
|
30
|
+
if (!handle)
|
|
31
|
+
return null;
|
|
32
|
+
const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
const headerMatch = text.match(new RegExp(`^[ \\t]*role:[ \\t]*(.+)\\r?\\n[ \\t]*nic-hdl:[ \\t]*${escaped}[ \\t]*$`, 'im'));
|
|
34
|
+
if (!headerMatch)
|
|
35
|
+
return null;
|
|
36
|
+
const lines = text
|
|
37
|
+
.slice((headerMatch.index ?? 0) + headerMatch[0].length)
|
|
38
|
+
.split('\n', 22);
|
|
39
|
+
const block = [];
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (!line.trim()) {
|
|
42
|
+
if (block.length)
|
|
43
|
+
break;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (/^[ \t]*(?:role|person|domain):/i.test(line))
|
|
47
|
+
break;
|
|
48
|
+
if (block.length >= 20)
|
|
49
|
+
break;
|
|
50
|
+
block.push(line);
|
|
51
|
+
}
|
|
52
|
+
const blockText = block.join('\n');
|
|
53
|
+
const get = (pattern, maxLength) => {
|
|
54
|
+
const match = blockText.match(pattern);
|
|
55
|
+
return match
|
|
56
|
+
? boundedWhoisValue(match[1], maxLength)
|
|
57
|
+
: { value: null, truncated: false };
|
|
58
|
+
};
|
|
59
|
+
const rawAddresses = [
|
|
60
|
+
...blockText.matchAll(/^[ \t]*address:[ \t]*(.+)$/gim),
|
|
61
|
+
];
|
|
62
|
+
const addresses = rawAddresses
|
|
63
|
+
.slice(0, 4)
|
|
64
|
+
.map((match) => boundedWhoisValue(match[1], 300))
|
|
65
|
+
.filter((entry) => entry.value);
|
|
66
|
+
const address = boundedWhoisValue(addresses.map((entry) => entry.value).join(', '), 1000);
|
|
67
|
+
return {
|
|
68
|
+
org: boundedWhoisValue(headerMatch[1], 300),
|
|
69
|
+
email: get(/^[ \t]*e-?mail:[ \t]*(.+)$/im, 320),
|
|
70
|
+
phone: get(/^[ \t]*phone:[ \t]*(.+)$/im, 100),
|
|
71
|
+
address,
|
|
72
|
+
truncated: block.length >= 20
|
|
73
|
+
|| rawAddresses.length > addresses.length
|
|
74
|
+
|| addresses.some((entry) => entry.truncated),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function resolveIrnicContact(text, handle) {
|
|
78
|
+
if (!handle)
|
|
79
|
+
return null;
|
|
80
|
+
const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
81
|
+
const headerMatch = text.match(new RegExp(`^[ \\t]*nic-hdl:[ \\t]*${escaped}[ \\t]*$`, 'im'));
|
|
82
|
+
if (!headerMatch)
|
|
83
|
+
return null;
|
|
84
|
+
const lines = text
|
|
85
|
+
.slice((headerMatch.index ?? 0) + headerMatch[0].length)
|
|
86
|
+
.split('\n', 22);
|
|
87
|
+
const block = [];
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
if (!line.trim()) {
|
|
90
|
+
if (block.length)
|
|
91
|
+
break;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (/^[ \t]*(?:domain|nic-hdl):/i.test(line))
|
|
95
|
+
break;
|
|
96
|
+
if (block.length >= 20)
|
|
97
|
+
break;
|
|
98
|
+
block.push(line);
|
|
99
|
+
}
|
|
100
|
+
const blockText = block.join('\n');
|
|
101
|
+
if (!/^[ \t]*source[ \t]*:[ \t]*IRNIC(?:\s|$)/im.test(blockText)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const get = (pattern, maxLength) => {
|
|
105
|
+
const match = blockText.match(pattern);
|
|
106
|
+
return match
|
|
107
|
+
? boundedWhoisValue(match[1], maxLength)
|
|
108
|
+
: { value: null, truncated: false };
|
|
109
|
+
};
|
|
110
|
+
const rawAddresses = [
|
|
111
|
+
...blockText.matchAll(/^[ \t]*address[ \t]*:[ \t]*(.+)$/gim),
|
|
112
|
+
];
|
|
113
|
+
const addresses = rawAddresses
|
|
114
|
+
.slice(0, 4)
|
|
115
|
+
.map((match) => boundedWhoisValue(match[1], 300))
|
|
116
|
+
.filter((entry) => entry.value);
|
|
117
|
+
const address = boundedWhoisValue(addresses.map((entry) => entry.value).join(', '), 1000);
|
|
118
|
+
return {
|
|
119
|
+
name: get(/^[ \t]*person[ \t]*:[ \t]*(.+)$/im, 300),
|
|
120
|
+
org: get(/^[ \t]*org[ \t]*:[ \t]*(.+)$/im, 300),
|
|
121
|
+
email: get(/^[ \t]*e-?mail[ \t]*:[ \t]*(.+)$/im, 320),
|
|
122
|
+
phone: get(/^[ \t]*phone[ \t]*:[ \t]*(.+)$/im, 100),
|
|
123
|
+
address,
|
|
124
|
+
truncated: block.length >= 20
|
|
125
|
+
|| rawAddresses.length > addresses.length
|
|
126
|
+
|| addresses.some((entry) => entry.truncated),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function parseIndentedContactBlock(text, headerRe) {
|
|
130
|
+
const headerMatch = text.match(headerRe);
|
|
131
|
+
if (!headerMatch)
|
|
132
|
+
return null;
|
|
133
|
+
const rest = text.slice((headerMatch.index ?? 0) + headerMatch[0].length);
|
|
134
|
+
const blankLineMatch = rest.match(/\n[ \t]*\n/);
|
|
135
|
+
const blockText = blankLineMatch
|
|
136
|
+
? rest.slice(0, blankLineMatch.index)
|
|
137
|
+
: rest;
|
|
138
|
+
const allLines = blockText
|
|
139
|
+
.split('\n')
|
|
140
|
+
.map((line) => line.trim())
|
|
141
|
+
.filter(Boolean);
|
|
142
|
+
const lines = allLines.slice(0, 20);
|
|
143
|
+
if (lines.length === 0)
|
|
144
|
+
return null;
|
|
145
|
+
const remaining = [...lines];
|
|
146
|
+
const emailIndex = remaining.findIndex((line) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(line));
|
|
147
|
+
const email = emailIndex !== -1
|
|
148
|
+
? remaining.splice(emailIndex, 1)[0]
|
|
149
|
+
: null;
|
|
150
|
+
const phoneIndex = remaining.findIndex((line) => /^[+\d][\d.\-() ]{6,}$/.test(line));
|
|
151
|
+
const phone = phoneIndex !== -1
|
|
152
|
+
? remaining.splice(phoneIndex, 1)[0]
|
|
153
|
+
: null;
|
|
154
|
+
const name = remaining.shift() || null;
|
|
155
|
+
const address = remaining.length ? remaining.join(', ') : null;
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
address,
|
|
159
|
+
phone,
|
|
160
|
+
email,
|
|
161
|
+
truncated: allLines.length > lines.length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Bounded normalization for imported and collected WHOIS referral chains.
|
|
2
|
+
// Keeping this separate lets authority analysis and field parsing share the
|
|
3
|
+
// exact same defensive input boundary.
|
|
4
|
+
import { MAX_WHOIS_BYTES } from "./whois-transport.mjs";
|
|
5
|
+
const MAX_WHOIS_HOPS = 6;
|
|
6
|
+
export function normalizeWhoisChain(value) {
|
|
7
|
+
if (!Array.isArray(value))
|
|
8
|
+
return [];
|
|
9
|
+
const normalized = [];
|
|
10
|
+
for (const hopValue of value.slice(0, MAX_WHOIS_HOPS)) {
|
|
11
|
+
if (!hopValue || typeof hopValue !== 'object' || Array.isArray(hopValue))
|
|
12
|
+
continue;
|
|
13
|
+
const hop = hopValue;
|
|
14
|
+
if (typeof hop.server !== 'string' || !hop.server.trim())
|
|
15
|
+
continue;
|
|
16
|
+
normalized.push({
|
|
17
|
+
server: hop.server.slice(0, 300),
|
|
18
|
+
...(typeof hop.address === 'string' || hop.address === null ? { address: hop.address } : {}),
|
|
19
|
+
...(typeof hop.queriedAt === 'string' ? { queriedAt: hop.queriedAt.slice(0, 64) } : {}),
|
|
20
|
+
...(typeof hop.queryProfile === 'string' ? { queryProfile: hop.queryProfile.slice(0, 80) } : {}),
|
|
21
|
+
...(typeof hop.responseEncoding === 'string' ? { responseEncoding: hop.responseEncoding.slice(0, 40) } : {}),
|
|
22
|
+
...(typeof hop.response === 'string' ? { response: hop.response.slice(0, MAX_WHOIS_BYTES) } : {}),
|
|
23
|
+
...(typeof hop.error === 'string' ? { error: hop.error.slice(0, 1000) } : {}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Bounded WHOIS port-43 transport with public-address validation and
|
|
2
|
+
// connection pinning. Referral orchestration and response parsing remain in
|
|
3
|
+
// lib/whois.mts so this module has one responsibility: safely query one
|
|
4
|
+
// validated WHOIS server.
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import { resolvePublicAddresses } from "./safe-fetch.mjs";
|
|
7
|
+
const MAX_WHOIS_BYTES = 200000;
|
|
8
|
+
const WHOIS_HOP_DEADLINE_MS = 12000;
|
|
9
|
+
const MAX_WHOIS_ADDRESSES = 3;
|
|
10
|
+
function errorMessage(value, fallback) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
12
|
+
return fallback;
|
|
13
|
+
const message = value.message;
|
|
14
|
+
return message ? String(message) : fallback;
|
|
15
|
+
}
|
|
16
|
+
// A referral hostname is upstream-controlled, so resolve and validate its
|
|
17
|
+
// addresses before connecting. Connect to the selected address directly to
|
|
18
|
+
// prevent a second DNS lookup from bypassing the public-address decision.
|
|
19
|
+
function queryWhoisAddress(address, server, query, { port = 43, timeoutMs = 10000, totalDeadlineMs = WHOIS_HOP_DEADLINE_MS, createConnection = net.createConnection, } = {}) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const socket = createConnection({ host: address, port }, () => {
|
|
22
|
+
socket.write(query + '\r\n');
|
|
23
|
+
});
|
|
24
|
+
const chunks = [];
|
|
25
|
+
let totalBytes = 0;
|
|
26
|
+
let settled = false;
|
|
27
|
+
const deadline = setTimeout(() => {
|
|
28
|
+
settled = true;
|
|
29
|
+
socket.destroy();
|
|
30
|
+
reject(new Error(`WHOIS request to ${server} exceeded the total time limit`));
|
|
31
|
+
}, totalDeadlineMs);
|
|
32
|
+
function settle(fn, value) {
|
|
33
|
+
if (settled)
|
|
34
|
+
return;
|
|
35
|
+
settled = true;
|
|
36
|
+
clearTimeout(deadline);
|
|
37
|
+
fn(value);
|
|
38
|
+
}
|
|
39
|
+
socket.setTimeout(Math.min(timeoutMs, totalDeadlineMs));
|
|
40
|
+
socket.on('data', (chunk) => {
|
|
41
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
42
|
+
totalBytes += buffer.length;
|
|
43
|
+
if (totalBytes > MAX_WHOIS_BYTES) {
|
|
44
|
+
socket.destroy();
|
|
45
|
+
settle(reject, new Error(`WHOIS response from ${server} exceeded ${MAX_WHOIS_BYTES} bytes`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(buffer);
|
|
49
|
+
});
|
|
50
|
+
const responseText = () => Buffer.concat(chunks, totalBytes).toString('utf8');
|
|
51
|
+
socket.on('end', () => settle(resolve, responseText()));
|
|
52
|
+
socket.on('close', () => settle(resolve, responseText()));
|
|
53
|
+
socket.on('timeout', () => {
|
|
54
|
+
socket.destroy();
|
|
55
|
+
settle(reject, new Error(`WHOIS request to ${server} timed out`));
|
|
56
|
+
});
|
|
57
|
+
socket.on('error', (error) => settle(reject, error));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function whoisQuery(server, query, { port = 43, timeoutMs = 10000, totalDeadlineMs = WHOIS_HOP_DEADLINE_MS, resolveAddresses = resolvePublicAddresses, queryAddress = queryWhoisAddress, now = Date.now, onAddressSelected = (_address) => { }, } = {}) {
|
|
61
|
+
const startedAt = now();
|
|
62
|
+
let resolutionTimer;
|
|
63
|
+
const records = await Promise.race([
|
|
64
|
+
resolveAddresses(server),
|
|
65
|
+
new Promise((_, reject) => {
|
|
66
|
+
resolutionTimer = setTimeout(() => reject(new Error(`WHOIS request to ${server} timed out during DNS resolution`)), totalDeadlineMs);
|
|
67
|
+
}),
|
|
68
|
+
]).finally(() => clearTimeout(resolutionTimer));
|
|
69
|
+
const candidates = records.slice(0, MAX_WHOIS_ADDRESSES);
|
|
70
|
+
const failures = [];
|
|
71
|
+
let attempts = 0;
|
|
72
|
+
for (const { address } of candidates) {
|
|
73
|
+
const remainingMs = totalDeadlineMs - (now() - startedAt);
|
|
74
|
+
if (remainingMs <= 0)
|
|
75
|
+
break;
|
|
76
|
+
attempts += 1;
|
|
77
|
+
try {
|
|
78
|
+
const response = await queryAddress(address, server, query, {
|
|
79
|
+
port,
|
|
80
|
+
timeoutMs: Math.min(timeoutMs, remainingMs),
|
|
81
|
+
totalDeadlineMs: remainingMs,
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
onAddressSelected(address);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Diagnostics must not break a successful lookup.
|
|
88
|
+
}
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
failures.push(errorMessage(error, 'connection failed').slice(0, 200));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const detail = failures.length
|
|
96
|
+
? `: ${failures.join('; ')}`
|
|
97
|
+
: ' because the total time limit expired';
|
|
98
|
+
throw new Error(`WHOIS request to ${server} failed after ${attempts} of ${candidates.length} validated address(es)${detail}`);
|
|
99
|
+
}
|
|
100
|
+
export { MAX_WHOIS_BYTES, queryWhoisAddress, whoisQuery, };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Shared bounded scalar and section helpers for WHOIS parsers. Registry
|
|
2
|
+
// branches may interpret different labels, but they all use the same control
|
|
3
|
+
// character, size, indentation, and section traversal rules.
|
|
4
|
+
export const MAX_WHOIS_FIELD_LENGTH = 1000;
|
|
5
|
+
export function boundedWhoisValue(value, maxLength = MAX_WHOIS_FIELD_LENGTH) {
|
|
6
|
+
if (typeof value !== 'string'
|
|
7
|
+
|| /[\u0000-\u0008\u000a-\u001f\u007f]/.test(value)) {
|
|
8
|
+
return { value: null, truncated: false };
|
|
9
|
+
}
|
|
10
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
11
|
+
if (!normalized)
|
|
12
|
+
return { value: null, truncated: false };
|
|
13
|
+
return {
|
|
14
|
+
value: normalized.slice(0, maxLength).trim() || null,
|
|
15
|
+
truncated: normalized.length > maxLength,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function parseIndentedWhoisValue(text, headerRe, maxLength) {
|
|
19
|
+
const headerMatch = text.match(headerRe);
|
|
20
|
+
if (!headerMatch)
|
|
21
|
+
return null;
|
|
22
|
+
const lines = text
|
|
23
|
+
.slice((headerMatch.index ?? 0) + headerMatch[0].length)
|
|
24
|
+
.split('\n')
|
|
25
|
+
.slice(0, 8);
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
if (!line.trim())
|
|
28
|
+
continue;
|
|
29
|
+
if (!/^[ \t]/.test(line))
|
|
30
|
+
return null;
|
|
31
|
+
return boundedWhoisValue(line, maxLength);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
export function parseIndentedWhoisSubfield(text, headerRe, subfieldRe, maxLength) {
|
|
36
|
+
const headerMatch = text.match(headerRe);
|
|
37
|
+
if (!headerMatch)
|
|
38
|
+
return null;
|
|
39
|
+
const lines = text
|
|
40
|
+
.slice((headerMatch.index ?? 0) + headerMatch[0].length)
|
|
41
|
+
.split('\n')
|
|
42
|
+
.slice(0, 8);
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (!line.trim())
|
|
45
|
+
continue;
|
|
46
|
+
if (!/^[ \t]/.test(line))
|
|
47
|
+
return null;
|
|
48
|
+
const match = line.match(subfieldRe);
|
|
49
|
+
if (match)
|
|
50
|
+
return boundedWhoisValue(match[1], maxLength);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function parseBoundedWhoisSection(text, headerRe, maxLines = 20) {
|
|
55
|
+
const headerMatch = text.match(headerRe);
|
|
56
|
+
if (!headerMatch)
|
|
57
|
+
return '';
|
|
58
|
+
const lines = text
|
|
59
|
+
.slice((headerMatch.index ?? 0) + headerMatch[0].length)
|
|
60
|
+
.split('\n', maxLines + 2);
|
|
61
|
+
const section = [];
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (!line.trim()) {
|
|
64
|
+
if (section.length)
|
|
65
|
+
break;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (/^[ \t]*\[[^\]]+\][ \t]*$/.test(line))
|
|
69
|
+
break;
|
|
70
|
+
if (section.length >= maxLines)
|
|
71
|
+
break;
|
|
72
|
+
section.push(line);
|
|
73
|
+
}
|
|
74
|
+
return section.join('\n');
|
|
75
|
+
}
|
|
76
|
+
export function whoisFieldLimit(key) {
|
|
77
|
+
if (/Email$/i.test(key))
|
|
78
|
+
return 320;
|
|
79
|
+
if (/Phone$/i.test(key))
|
|
80
|
+
return 100;
|
|
81
|
+
if (/Date$/i.test(key))
|
|
82
|
+
return 100;
|
|
83
|
+
if (/Url$/i.test(key))
|
|
84
|
+
return 2048;
|
|
85
|
+
if (/domainName/i.test(key))
|
|
86
|
+
return 253;
|
|
87
|
+
if (/Address/i.test(key))
|
|
88
|
+
return 1000;
|
|
89
|
+
if (/Street/i.test(key))
|
|
90
|
+
return 1000;
|
|
91
|
+
return 300;
|
|
92
|
+
}
|