@cleandns/whois-rdap 1.0.52 → 1.0.55
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/.github/workflows/test.yml +38 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +69 -88
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +360 -0
- package/dist/ip.js +8 -1
- package/dist/polyfills.d.ts +11 -0
- package/dist/polyfills.js +64 -0
- package/dist/port43.js +28 -14
- package/dist/utils/escapeRegex.d.ts +5 -0
- package/dist/utils/escapeRegex.js +7 -0
- package/dist/utils/findInObject.js +14 -4
- package/dist/utils/findNameservers.d.ts +1 -0
- package/dist/utils/findNameservers.js +15 -0
- package/dist/utils/findStatus.d.ts +1 -0
- package/dist/utils/findStatus.js +10 -0
- package/dist/utils/findTimestamps.d.ts +6 -0
- package/dist/utils/findTimestamps.js +39 -0
- package/dist/utils/toArray.d.ts +5 -0
- package/dist/utils/toArray.js +17 -0
- package/dist/utils/validateDomain.d.ts +5 -0
- package/dist/utils/validateDomain.js +18 -0
- package/github-workflow.patch +1044 -0
- package/package.json +24 -17
- package/src/index.test.ts +440 -0
- package/src/index.ts +65 -89
- package/src/ip.ts +9 -1
- package/src/polyfills.ts +76 -0
- package/src/port43.ts +29 -21
- package/src/utils/escapeRegex.ts +7 -0
- package/src/utils/findInObject.ts +19 -4
- package/src/utils/findNameservers.ts +15 -0
- package/src/utils/findStatus.ts +12 -0
- package/src/utils/findTimestamps.ts +44 -0
- package/src/utils/toArray.ts +17 -0
- package/src/utils/validateDomain.ts +18 -0
- package/tsconfig.json +1 -1
- package/dist/wwwservers.d.ts +0 -1
- package/dist/wwwservers.js +0 -3
- package/pnpm-workspace.yaml +0 -2
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Polyfills for Node.js 18 compatibility
|
|
2
|
+
import "./polyfills.js";
|
|
3
|
+
|
|
1
4
|
import { WhoisOptions, WhoisResponse, WhoisTimestampFields } from "../whois.js";
|
|
2
5
|
import { parseIpResponse } from "./ip.js";
|
|
3
6
|
import { determinePort43Domain, port43 } from "./port43.js";
|
|
@@ -5,10 +8,19 @@ import { findInObject } from "./utils/findInObject.js";
|
|
|
5
8
|
import { fixArrays } from "./utils/fixArrays.js";
|
|
6
9
|
import { ianaIdToRegistrar } from "./utils/ianaIdToRegistrar.js";
|
|
7
10
|
import { tldToRdap } from "./utils/tldToRdap.js";
|
|
8
|
-
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
9
11
|
import { resolve4 } from "dns/promises";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
import { toArray } from "./utils/toArray.js";
|
|
13
|
+
import { validateDomain } from "./utils/validateDomain.js";
|
|
14
|
+
import { findStatus } from "./utils/findStatus.js";
|
|
15
|
+
import { findNameservers } from "./utils/findNameservers.js";
|
|
16
|
+
import { findTimestamps } from "./utils/findTimestamps.js";
|
|
17
|
+
import createDebug from "debug";
|
|
18
|
+
import { escapeRegex } from "./utils/escapeRegex.js";
|
|
19
|
+
|
|
20
|
+
// Debug logger - enable with DEBUG=whois:* environment variable
|
|
21
|
+
const debug = createDebug("whois:rdap");
|
|
22
|
+
|
|
23
|
+
export const eventMap = new Map<string, WhoisTimestampFields>([
|
|
12
24
|
["registration", "created"],
|
|
13
25
|
["last changed", "updated"],
|
|
14
26
|
["expiration", "expires"],
|
|
@@ -21,10 +33,27 @@ export async function whois(
|
|
|
21
33
|
): Promise<WhoisResponse> {
|
|
22
34
|
const _fetch = options.fetch || fetch;
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
// Validate and sanitize input
|
|
37
|
+
let domain: string;
|
|
38
|
+
try {
|
|
39
|
+
domain = validateDomain(origDomain);
|
|
40
|
+
} catch (e: any) {
|
|
41
|
+
return {
|
|
42
|
+
found: false,
|
|
43
|
+
statusCode: 400,
|
|
44
|
+
error: e.message,
|
|
45
|
+
registrar: { id: 0, name: null },
|
|
46
|
+
reseller: null,
|
|
47
|
+
status: [],
|
|
48
|
+
statusDelta: [],
|
|
49
|
+
nameservers: [],
|
|
50
|
+
ts: { created: null, updated: null, expires: null },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
let url: string | null = null;
|
|
26
55
|
|
|
27
|
-
[domain, url] = await tldToRdap(
|
|
56
|
+
[domain, url] = await tldToRdap(domain);
|
|
28
57
|
|
|
29
58
|
const response: WhoisResponse = {
|
|
30
59
|
found: false,
|
|
@@ -59,7 +88,6 @@ export async function whois(
|
|
|
59
88
|
thinResponse = await _fetch(thinRdap)
|
|
60
89
|
.then((r) => {
|
|
61
90
|
response.statusCode = r.status;
|
|
62
|
-
// console.log({ ok: r.ok, status: r.status, statusText: r.statusText });
|
|
63
91
|
if (r.status >= 200 && r.status < 400) {
|
|
64
92
|
return r.json() as any;
|
|
65
93
|
}
|
|
@@ -67,7 +95,7 @@ export async function whois(
|
|
|
67
95
|
return null;
|
|
68
96
|
})
|
|
69
97
|
.catch((error: Error) => {
|
|
70
|
-
|
|
98
|
+
debug("thin RDAP lookup failure for %s: %s", domain, error.message);
|
|
71
99
|
return null;
|
|
72
100
|
});
|
|
73
101
|
|
|
@@ -94,14 +122,14 @@ export async function whois(
|
|
|
94
122
|
let thickResponse: any = null;
|
|
95
123
|
|
|
96
124
|
if (!options.thinOnly && thickRdap) {
|
|
97
|
-
|
|
125
|
+
debug("fetching thick RDAP: %s", thickRdap);
|
|
98
126
|
thickResponse = await _fetch(thickRdap)
|
|
99
127
|
.then((r) => r.json() as any)
|
|
100
128
|
.catch(() => null);
|
|
101
129
|
if (thickResponse && !thickResponse.errorCode && !thickResponse.error) {
|
|
102
130
|
} else {
|
|
103
131
|
thickResponse = null;
|
|
104
|
-
|
|
132
|
+
debug("thick RDAP failed for %s", domain);
|
|
105
133
|
}
|
|
106
134
|
}
|
|
107
135
|
|
|
@@ -113,10 +141,14 @@ export async function whois(
|
|
|
113
141
|
const resellers: any[] = [];
|
|
114
142
|
|
|
115
143
|
async function extractRegistrarsAndResellers(response: any, url: string, isThick?: boolean) {
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
// Use toArray to safely handle entities that might not be iterable
|
|
145
|
+
const entities = toArray(response.entities);
|
|
146
|
+
const entityList = [
|
|
147
|
+
...entities,
|
|
118
148
|
response.entity ? { events: response.events, ...response.entity } : null,
|
|
119
|
-
].filter(Boolean)
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
|
|
151
|
+
for (const ent of entityList) {
|
|
120
152
|
if (ent.roles?.includes("registrar") || ent.role === "registrar") {
|
|
121
153
|
const pubIds: any[] = [];
|
|
122
154
|
if (ent.publicIds) {
|
|
@@ -145,7 +177,6 @@ export async function whois(
|
|
|
145
177
|
;
|
|
146
178
|
|
|
147
179
|
if (reg) {
|
|
148
|
-
// console.log(ent.vcardArray);
|
|
149
180
|
const id = typeof reg === 'object' ? 0 : reg;
|
|
150
181
|
const name =
|
|
151
182
|
(parseInt(id) == id
|
|
@@ -157,8 +188,10 @@ export async function whois(
|
|
|
157
188
|
(el: any[]) => el[3],
|
|
158
189
|
reg
|
|
159
190
|
);
|
|
191
|
+
// Safely handle ent.entities
|
|
192
|
+
const entEntities = toArray(ent.entities);
|
|
160
193
|
const email =
|
|
161
|
-
[ent, ...
|
|
194
|
+
[ent, ...entEntities]
|
|
162
195
|
.filter((e) => e?.vcardArray)
|
|
163
196
|
.map((e) =>
|
|
164
197
|
findInObject(
|
|
@@ -171,7 +204,7 @@ export async function whois(
|
|
|
171
204
|
.filter(Boolean)?.[0] || "";
|
|
172
205
|
|
|
173
206
|
const abuseEmail =
|
|
174
|
-
[ent, ...
|
|
207
|
+
[ent, ...entEntities]
|
|
175
208
|
.filter((e) => e?.vcardArray)
|
|
176
209
|
.map((e) =>
|
|
177
210
|
findInObject(
|
|
@@ -187,10 +220,11 @@ export async function whois(
|
|
|
187
220
|
ent.events || response.events || ent.enents || response.enents;
|
|
188
221
|
registrars.push({ id, name, email, abuseEmail, events });
|
|
189
222
|
}
|
|
190
|
-
// handles .ca
|
|
223
|
+
// handles .ca - with safe optional chaining
|
|
191
224
|
else if (ent.vcardArray?.[1]?.[3]?.[3] === 'registrar') {
|
|
225
|
+
const entEntities = toArray(ent.entities);
|
|
192
226
|
const email =
|
|
193
|
-
[ent, ...
|
|
227
|
+
[ent, ...entEntities]
|
|
194
228
|
.filter((e) => e?.vcardArray)
|
|
195
229
|
.map((e) =>
|
|
196
230
|
findInObject(
|
|
@@ -203,7 +237,7 @@ export async function whois(
|
|
|
203
237
|
.filter(Boolean)?.[0] || "";
|
|
204
238
|
|
|
205
239
|
const abuseEmail =
|
|
206
|
-
[ent, ...
|
|
240
|
+
[ent, ...entEntities]
|
|
207
241
|
.filter((e) => e?.vcardArray)
|
|
208
242
|
.map((e) =>
|
|
209
243
|
findInObject(
|
|
@@ -215,12 +249,14 @@ export async function whois(
|
|
|
215
249
|
)
|
|
216
250
|
.filter(Boolean)?.[0] || "";
|
|
217
251
|
|
|
218
|
-
|
|
252
|
+
const vcardName = ent.vcardArray?.[1]?.[1]?.[3] || '';
|
|
253
|
+
registrars.push({ id: 0, name: vcardName, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
219
254
|
}
|
|
220
|
-
// handles .si
|
|
221
|
-
else if (ent.vcardArray && ent.vcardArray[1] && ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')) {
|
|
255
|
+
// handles .si - with safe array access
|
|
256
|
+
else if (ent.vcardArray && Array.isArray(ent.vcardArray[1]) && ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')) {
|
|
257
|
+
const entEntities = toArray(ent.entities);
|
|
222
258
|
const email =
|
|
223
|
-
[ent, ...
|
|
259
|
+
[ent, ...entEntities]
|
|
224
260
|
.filter((e) => e?.vcardArray)
|
|
225
261
|
.map((e) =>
|
|
226
262
|
findInObject(
|
|
@@ -233,7 +269,7 @@ export async function whois(
|
|
|
233
269
|
.filter(Boolean)?.[0] || "";
|
|
234
270
|
|
|
235
271
|
const abuseEmail =
|
|
236
|
-
[ent, ...
|
|
272
|
+
[ent, ...entEntities]
|
|
237
273
|
.filter((e) => e?.vcardArray)
|
|
238
274
|
.map((e) =>
|
|
239
275
|
findInObject(
|
|
@@ -260,7 +296,9 @@ export async function whois(
|
|
|
260
296
|
registrars.push({ id, name, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
261
297
|
}
|
|
262
298
|
else {
|
|
263
|
-
|
|
299
|
+
const fnEntry = ent.vcardArray[1].find((el: string[]) => el[0] === 'fn');
|
|
300
|
+
const name = fnEntry ? fnEntry[3] : ent.handle || '';
|
|
301
|
+
registrars.push({ id: ent.handle || 0, name, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
264
302
|
}
|
|
265
303
|
}
|
|
266
304
|
// handles .ar
|
|
@@ -285,8 +323,9 @@ export async function whois(
|
|
|
285
323
|
(el: any[]) => el[3],
|
|
286
324
|
id
|
|
287
325
|
);
|
|
326
|
+
const entEntities = toArray(ent.entities);
|
|
288
327
|
const email =
|
|
289
|
-
[ent, ...
|
|
328
|
+
[ent, ...entEntities]
|
|
290
329
|
.filter((e) => e?.vcardArray)
|
|
291
330
|
.map((e) =>
|
|
292
331
|
findInObject(
|
|
@@ -299,7 +338,7 @@ export async function whois(
|
|
|
299
338
|
.filter(Boolean)?.[0] || "";
|
|
300
339
|
|
|
301
340
|
const abuseEmail =
|
|
302
|
-
[ent, ...
|
|
341
|
+
[ent, ...entEntities]
|
|
303
342
|
.filter((e) => e?.vcardArray)
|
|
304
343
|
.map((e) =>
|
|
305
344
|
findInObject(
|
|
@@ -392,66 +431,3 @@ export async function whois(
|
|
|
392
431
|
|
|
393
432
|
return response;
|
|
394
433
|
}
|
|
395
|
-
|
|
396
|
-
function findStatus(statuses: string | string[], domain: string): string[] {
|
|
397
|
-
// console.warn({ domain, statuses });
|
|
398
|
-
|
|
399
|
-
return (Array.isArray(statuses)
|
|
400
|
-
? statuses
|
|
401
|
-
: statuses && typeof statuses === "object"
|
|
402
|
-
? Object.keys(statuses)
|
|
403
|
-
: typeof statuses === "string"
|
|
404
|
-
? statuses.trim().split(/\s*,\s*/)
|
|
405
|
-
: []
|
|
406
|
-
).map((status) => normalizeWhoisStatus(status));
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function findNameservers(values: any[]): string[] {
|
|
410
|
-
let nameservers: any[] = [];
|
|
411
|
-
if (Array.isArray(values)) {
|
|
412
|
-
nameservers = values;
|
|
413
|
-
} else if (typeof values === "object") {
|
|
414
|
-
nameservers = Object.values(values);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return nameservers
|
|
418
|
-
.map((ns) => ns.ldhName || ns.ldnName || ns.ipAddresses?.v4)
|
|
419
|
-
.flat()
|
|
420
|
-
.filter((ns) => ns)
|
|
421
|
-
.map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
|
|
422
|
-
.sort();
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function findTimestamps(values: any[]) {
|
|
426
|
-
const ts: Record<WhoisTimestampFields, Date | null> = {
|
|
427
|
-
created: null,
|
|
428
|
-
updated: null,
|
|
429
|
-
expires: null,
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
let events: any = [];
|
|
433
|
-
|
|
434
|
-
if (Array.isArray(values)) {
|
|
435
|
-
events = values;
|
|
436
|
-
} else if (typeof values === "object") {
|
|
437
|
-
events = Object.values(values);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
for (const [event, field] of eventMap) {
|
|
441
|
-
events.find(
|
|
442
|
-
(ev: any) => {
|
|
443
|
-
const isMatch = ev?.eventAction?.toLocaleLowerCase() === event && ev.eventDate;
|
|
444
|
-
if (isMatch) {
|
|
445
|
-
const d = new Date(ev.eventDate.toString().replace(/\+0000Z$/, "Z"));
|
|
446
|
-
// console.log(field, ev.eventDate, d);
|
|
447
|
-
if (!isNaN(d.valueOf())) {
|
|
448
|
-
ts[field] = d;
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return ts;
|
|
457
|
-
}
|
package/src/ip.ts
CHANGED
|
@@ -3,7 +3,15 @@ import { WhoisResponse } from "../whois.js";
|
|
|
3
3
|
export function parseIpResponse(ip: string, rdap: any, response: WhoisResponse) {
|
|
4
4
|
response.found = Boolean(rdap.handle);
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Safely extract registry from port43 with null check
|
|
7
|
+
let registry = '';
|
|
8
|
+
if (rdap.port43) {
|
|
9
|
+
const match = rdap.port43.match(/\.(\w+)\./);
|
|
10
|
+
if (match) {
|
|
11
|
+
registry = match[1].toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
const realRdapServer = rdap.links?.find(({ rel }: { rel: string }) => rel === 'self')?.value?.replace(/\/ip\/.*/, '/ip/');
|
|
8
16
|
|
|
9
17
|
response.server = realRdapServer || 'https://rdap.org/ip/';
|
package/src/polyfills.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyfills for Node.js 18 compatibility.
|
|
3
|
+
* String.prototype.toWellFormed was added in Node.js 20.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Extend String interface for TypeScript
|
|
7
|
+
declare global {
|
|
8
|
+
interface String {
|
|
9
|
+
toWellFormed(): string;
|
|
10
|
+
isWellFormed(): boolean;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Polyfill for String.prototype.toWellFormed (ES2024)
|
|
15
|
+
// Replaces lone surrogates with U+FFFD (replacement character)
|
|
16
|
+
if (typeof String.prototype.toWellFormed !== 'function') {
|
|
17
|
+
String.prototype.toWellFormed = function () {
|
|
18
|
+
const str = String(this);
|
|
19
|
+
const len = str.length;
|
|
20
|
+
let result = '';
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < len; i++) {
|
|
23
|
+
const code = str.charCodeAt(i);
|
|
24
|
+
|
|
25
|
+
// Check for lone surrogates
|
|
26
|
+
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
27
|
+
// High surrogate
|
|
28
|
+
if (i + 1 < len) {
|
|
29
|
+
const next = str.charCodeAt(i + 1);
|
|
30
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
31
|
+
// Valid surrogate pair
|
|
32
|
+
result += str[i] + str[i + 1];
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Lone high surrogate - replace with U+FFFD
|
|
38
|
+
result += '\uFFFD';
|
|
39
|
+
} else if (code >= 0xDC00 && code <= 0xDFFF) {
|
|
40
|
+
// Lone low surrogate - replace with U+FFFD
|
|
41
|
+
result += '\uFFFD';
|
|
42
|
+
} else {
|
|
43
|
+
result += str[i];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Polyfill for String.prototype.isWellFormed (ES2024)
|
|
52
|
+
if (typeof String.prototype.isWellFormed !== 'function') {
|
|
53
|
+
String.prototype.isWellFormed = function () {
|
|
54
|
+
const str = String(this);
|
|
55
|
+
const len = str.length;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < len; i++) {
|
|
58
|
+
const code = str.charCodeAt(i);
|
|
59
|
+
|
|
60
|
+
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
61
|
+
// High surrogate - check for valid pair
|
|
62
|
+
if (i + 1 >= len) return false;
|
|
63
|
+
const next = str.charCodeAt(i + 1);
|
|
64
|
+
if (next < 0xDC00 || next > 0xDFFF) return false;
|
|
65
|
+
i++; // Skip the low surrogate
|
|
66
|
+
} else if (code >= 0xDC00 && code <= 0xDFFF) {
|
|
67
|
+
// Lone low surrogate
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {};
|
package/src/port43.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { port43servers, port43parsers } from "./port43servers.js";
|
|
|
5
5
|
import { ianaToRegistrarCache } from "./utils/ianaIdToRegistrar.js";
|
|
6
6
|
import { WhoisResponse } from "../whois.js";
|
|
7
7
|
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
8
|
+
import createDebug from "debug";
|
|
9
|
+
import { escapeRegex } from "./utils/escapeRegex.js";
|
|
10
|
+
|
|
11
|
+
// Debug logger - enable with DEBUG=whois:* environment variable
|
|
12
|
+
const debug = createDebug("whois:port43");
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
export function determinePort43Domain(actor: string) {
|
|
10
16
|
const parsed = parseDomain(actor);
|
|
@@ -36,7 +42,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
|
36
42
|
: `${domain}\r\n`;
|
|
37
43
|
const port = opts?.port || 43;
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
debug("looking up %s on %s:%d", domain, server, port);
|
|
40
46
|
|
|
41
47
|
const response: WhoisResponse = {
|
|
42
48
|
found: true,
|
|
@@ -82,7 +88,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
} catch (error: any) {
|
|
85
|
-
|
|
91
|
+
debug("port43 lookup error: %O", { port, server, query, error: error.message });
|
|
86
92
|
response.found = false;
|
|
87
93
|
response.statusCode = 500;
|
|
88
94
|
response.error = error.message || "Unknown error during port 43 lookup";
|
|
@@ -93,7 +99,6 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
port43response = port43response.replace(/^[ \t]+/gm, "");
|
|
96
|
-
// console.log(port43response);
|
|
97
102
|
|
|
98
103
|
let m;
|
|
99
104
|
|
|
@@ -145,12 +150,6 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
|
145
150
|
)) &&
|
|
146
151
|
(response.reseller = m[1].trim());
|
|
147
152
|
|
|
148
|
-
// console.log(port43response)
|
|
149
|
-
|
|
150
|
-
// Updated Date: 2024-11-21T13:42:54Z
|
|
151
|
-
// Creation Date: 2017-12-16T02:11:08Z
|
|
152
|
-
// Registry Expiry Date: 2031-07-10T02:11:08Z
|
|
153
|
-
|
|
154
153
|
!response.ts.updated &&
|
|
155
154
|
(m = port43response.match(
|
|
156
155
|
/^(?:Last Modified|Updated Date|Last updated on|domain_datelastmodified|last-update|modified|last modified)\.*:[ \t]*(\S.+)/im
|
|
@@ -235,6 +234,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
|
235
234
|
if (response.ts.updated && !response.ts.updated.valueOf()) response.ts.updated = null;
|
|
236
235
|
if (response.ts.expires && !response.ts.expires.valueOf()) response.ts.expires = null;
|
|
237
236
|
|
|
237
|
+
// Match registrar name against IANA cache using escaped regex to prevent ReDoS
|
|
238
238
|
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
239
239
|
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
240
240
|
if (name === response.registrar.name) {
|
|
@@ -244,24 +244,32 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
247
|
+
if (response.registrar.id === 0 && response.registrar.name && response.registrar.name !== "") {
|
|
248
|
+
const escapedName = escapeRegex(response.registrar.name);
|
|
248
249
|
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
try {
|
|
251
|
+
if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
|
|
252
|
+
response.registrar.id = id;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Skip if regex still fails for some reason
|
|
257
|
+
continue;
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
if (response.registrar.id === 0 && response.registrar.name) {
|
|
263
|
+
const escapedName = escapeRegex(response.registrar.name.replace(/,.*/, ""));
|
|
257
264
|
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
258
|
-
|
|
259
|
-
name.match(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
try {
|
|
266
|
+
if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
|
|
267
|
+
response.registrar.id = id;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Skip if regex still fails for some reason
|
|
272
|
+
continue;
|
|
265
273
|
}
|
|
266
274
|
}
|
|
267
275
|
}
|
|
@@ -292,4 +300,4 @@ function reformatDate(date: string) {
|
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
return date;
|
|
295
|
-
}
|
|
303
|
+
}
|
|
@@ -7,17 +7,32 @@ export function findInObject(
|
|
|
7
7
|
const found = _findInObject(obj, condition);
|
|
8
8
|
return found === undefined ? fallback : extractor(found);
|
|
9
9
|
}
|
|
10
|
+
|
|
10
11
|
function _findInObject(obj: any, condition: (el: any) => boolean): any {
|
|
12
|
+
// Handle null/undefined
|
|
13
|
+
if (obj === null || obj === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
for (const key in obj) {
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
// Skip inherited properties
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
20
|
+
continue;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const value = obj[key];
|
|
24
|
+
|
|
25
|
+
if (condition(value)) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value !== null && typeof value === "object") {
|
|
30
|
+
const result = _findInObject(value, condition);
|
|
18
31
|
if (result !== undefined) {
|
|
19
32
|
return result;
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
23
38
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function findNameservers(values: any[]): string[] {
|
|
2
|
+
let nameservers: any[] = [];
|
|
3
|
+
if (Array.isArray(values)) {
|
|
4
|
+
nameservers = values;
|
|
5
|
+
} else if (typeof values === "object") {
|
|
6
|
+
nameservers = Object.values(values);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return nameservers
|
|
10
|
+
.map((ns) => ns.ldhName || ns.ldnName || ns.ipAddresses?.v4)
|
|
11
|
+
.flat()
|
|
12
|
+
.filter((ns) => ns)
|
|
13
|
+
.map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { normalizeWhoisStatus } from "../whoisStatus.js";
|
|
2
|
+
|
|
3
|
+
export function findStatus(statuses: string | string[], domain: string): string[] {
|
|
4
|
+
return (Array.isArray(statuses)
|
|
5
|
+
? statuses
|
|
6
|
+
: statuses && typeof statuses === "object"
|
|
7
|
+
? Object.keys(statuses)
|
|
8
|
+
: typeof statuses === "string"
|
|
9
|
+
? statuses.trim().split(/\s*,\s*/)
|
|
10
|
+
: []
|
|
11
|
+
).map((status) => normalizeWhoisStatus(status));
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { WhoisTimestampFields } from "../../whois.js";
|
|
2
|
+
import { eventMap } from "../index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracts timestamps from RDAP events array.
|
|
6
|
+
* Properly iterates through events and breaks when a match is found.
|
|
7
|
+
*/
|
|
8
|
+
export function findTimestamps(values: any[]) {
|
|
9
|
+
const ts: Record<WhoisTimestampFields, Date | null> = {
|
|
10
|
+
created: null,
|
|
11
|
+
updated: null,
|
|
12
|
+
expires: null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let events: any[] = [];
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(values)) {
|
|
18
|
+
events = values;
|
|
19
|
+
} else if (typeof values === "object" && values !== null) {
|
|
20
|
+
events = Object.values(values);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Iterate through each event type we're looking for
|
|
24
|
+
for (const [eventAction, field] of eventMap) {
|
|
25
|
+
// Skip if we already have a value for this field
|
|
26
|
+
if (ts[field] !== null) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Find matching event and extract date
|
|
31
|
+
for (const ev of events) {
|
|
32
|
+
if (ev?.eventAction?.toLocaleLowerCase() === eventAction && ev.eventDate) {
|
|
33
|
+
const dateStr = ev.eventDate.toString().replace(/\+0000Z$/, "Z");
|
|
34
|
+
const d = new Date(dateStr);
|
|
35
|
+
if (!isNaN(d.valueOf())) {
|
|
36
|
+
ts[field] = d;
|
|
37
|
+
break; // Found valid date, stop searching for this field
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ts;
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely converts a value to an array.
|
|
3
|
+
* Handles cases where the value might be null, undefined, or a non-iterable object.
|
|
4
|
+
*/
|
|
5
|
+
export function toArray<T>(value: T | T[] | null | undefined): T[] {
|
|
6
|
+
if (value === null || value === undefined) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
// Handle object case - some RDAP responses return objects instead of arrays
|
|
13
|
+
if (typeof value === "object") {
|
|
14
|
+
return Object.values(value) as T[];
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates domain input format.
|
|
3
|
+
* Returns sanitized domain or throws on invalid input.
|
|
4
|
+
*/
|
|
5
|
+
export function validateDomain(domain: string): string {
|
|
6
|
+
if (!domain || typeof domain !== 'string') {
|
|
7
|
+
throw new Error('Domain must be a non-empty string');
|
|
8
|
+
}
|
|
9
|
+
// Basic sanitization - trim whitespace
|
|
10
|
+
const sanitized = domain.trim().toLowerCase();
|
|
11
|
+
if (sanitized.length === 0) {
|
|
12
|
+
throw new Error('Domain must be a non-empty string');
|
|
13
|
+
}
|
|
14
|
+
if (sanitized.length > 253) {
|
|
15
|
+
throw new Error('Domain name too long');
|
|
16
|
+
}
|
|
17
|
+
return sanitized;
|
|
18
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
/* Modules */
|
|
28
28
|
"module": "nodenext", /* Specify what module code is generated. */
|
|
29
|
-
|
|
29
|
+
"rootDir": "./src", /* Specify the root folder within your source files. */
|
|
30
30
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
31
31
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
32
32
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
package/dist/wwwservers.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const loadWWWServers: Record<string, string>;
|
package/dist/wwwservers.js
DELETED
package/pnpm-workspace.yaml
DELETED