@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
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
|
|
2
|
+
new file mode 100644
|
|
3
|
+
index 0000000..9ecd388
|
|
4
|
+
--- /dev/null
|
|
5
|
+
+++ b/.github/workflows/test.yml
|
|
6
|
+
@@ -0,0 +1,38 @@
|
|
7
|
+
+name: Test
|
|
8
|
+
+
|
|
9
|
+
+on:
|
|
10
|
+
+ push:
|
|
11
|
+
+ branches: [main, master]
|
|
12
|
+
+ pull_request:
|
|
13
|
+
+ branches: [main, master]
|
|
14
|
+
+
|
|
15
|
+
+jobs:
|
|
16
|
+
+ test:
|
|
17
|
+
+ runs-on: ubuntu-latest
|
|
18
|
+
+
|
|
19
|
+
+ strategy:
|
|
20
|
+
+ matrix:
|
|
21
|
+
+ node-version: [18, 20, 22]
|
|
22
|
+
+
|
|
23
|
+
+ steps:
|
|
24
|
+
+ - uses: actions/checkout@v4
|
|
25
|
+
+
|
|
26
|
+
+ - name: Setup pnpm
|
|
27
|
+
+ uses: pnpm/action-setup@v4
|
|
28
|
+
+ with:
|
|
29
|
+
+ version: 9
|
|
30
|
+
+
|
|
31
|
+
+ - name: Setup Node.js ${{ matrix.node-version }}
|
|
32
|
+
+ uses: actions/setup-node@v4
|
|
33
|
+
+ with:
|
|
34
|
+
+ node-version: ${{ matrix.node-version }}
|
|
35
|
+
+ cache: pnpm
|
|
36
|
+
+
|
|
37
|
+
+ - name: Install dependencies
|
|
38
|
+
+ run: pnpm install
|
|
39
|
+
+
|
|
40
|
+
+ - name: Build
|
|
41
|
+
+ run: pnpm run build
|
|
42
|
+
+
|
|
43
|
+
+ - name: Run tests
|
|
44
|
+
+ run: pnpm test
|
|
45
|
+
diff --git a/package.json b/package.json
|
|
46
|
+
index 82d953a..5c966d9 100644
|
|
47
|
+
--- a/package.json
|
|
48
|
+
+++ b/package.json
|
|
49
|
+
@@ -6,6 +6,7 @@
|
|
50
|
+
"main": "dist/index.js",
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
+ "test": "node --test dist/*.test.js",
|
|
54
|
+
"upload": "npm run build && npm version patch && npm publish"
|
|
55
|
+
},
|
|
56
|
+
"repository": {
|
|
57
|
+
@@ -19,10 +20,12 @@
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/cleandns-inc/tool-whois#readme",
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
+ "@types/debug": "^4.1.12",
|
|
62
|
+
"@types/node": "^22.0.2",
|
|
63
|
+
"typescript": "^5.4.3"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
+ "debug": "^4.4.3",
|
|
67
|
+
"parse-domain": "^8.0.2",
|
|
68
|
+
"promise-socket": "^7.0.0"
|
|
69
|
+
}
|
|
70
|
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
|
|
71
|
+
index 9ecac82..8c88621 100644
|
|
72
|
+
--- a/pnpm-lock.yaml
|
|
73
|
+
+++ b/pnpm-lock.yaml
|
|
74
|
+
@@ -8,6 +8,9 @@ importers:
|
|
75
|
+
|
|
76
|
+
.:
|
|
77
|
+
dependencies:
|
|
78
|
+
+ debug:
|
|
79
|
+
+ specifier: ^4.4.3
|
|
80
|
+
+ version: 4.4.3
|
|
81
|
+
parse-domain:
|
|
82
|
+
specifier: ^8.0.2
|
|
83
|
+
version: 8.2.2
|
|
84
|
+
@@ -15,6 +18,9 @@ importers:
|
|
85
|
+
specifier: ^7.0.0
|
|
86
|
+
version: 7.0.0
|
|
87
|
+
devDependencies:
|
|
88
|
+
+ '@types/debug':
|
|
89
|
+
+ specifier: ^4.1.12
|
|
90
|
+
+ version: 4.1.12
|
|
91
|
+
'@types/node':
|
|
92
|
+
specifier: ^22.0.2
|
|
93
|
+
version: 22.15.33
|
|
94
|
+
@@ -24,6 +30,12 @@ importers:
|
|
95
|
+
|
|
96
|
+
packages:
|
|
97
|
+
|
|
98
|
+
+ '@types/debug@4.1.12':
|
|
99
|
+
+ resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
|
100
|
+
+
|
|
101
|
+
+ '@types/ms@2.1.0':
|
|
102
|
+
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
|
103
|
+
+
|
|
104
|
+
'@types/node@22.15.33':
|
|
105
|
+
resolution: {integrity: sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==}
|
|
106
|
+
|
|
107
|
+
@@ -38,6 +50,15 @@ packages:
|
|
108
|
+
core-js@3.43.0:
|
|
109
|
+
resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==}
|
|
110
|
+
|
|
111
|
+
+ debug@4.4.3:
|
|
112
|
+
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
|
113
|
+
+ engines: {node: '>=6.0'}
|
|
114
|
+
+ peerDependencies:
|
|
115
|
+
+ supports-color: '*'
|
|
116
|
+
+ peerDependenciesMeta:
|
|
117
|
+
+ supports-color:
|
|
118
|
+
+ optional: true
|
|
119
|
+
+
|
|
120
|
+
function-timeout@0.1.1:
|
|
121
|
+
resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==}
|
|
122
|
+
engines: {node: '>=14.16'}
|
|
123
|
+
@@ -54,6 +75,9 @@ packages:
|
|
124
|
+
resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
|
|
125
|
+
engines: {node: '>=12'}
|
|
126
|
+
|
|
127
|
+
+ ms@2.1.3:
|
|
128
|
+
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
|
129
|
+
+
|
|
130
|
+
parse-domain@8.2.2:
|
|
131
|
+
resolution: {integrity: sha512-CoksenD3UDqphCHlXIcNh/TX0dsYLHo6dSAUC/QBcJRWJXcV5rc1mwsS4WbhYGu4LD4Uxc0v3ZzGo+OHCGsLcw==}
|
|
132
|
+
hasBin: true
|
|
133
|
+
@@ -95,6 +119,12 @@ packages:
|
|
134
|
+
|
|
135
|
+
snapshots:
|
|
136
|
+
|
|
137
|
+
+ '@types/debug@4.1.12':
|
|
138
|
+
+ dependencies:
|
|
139
|
+
+ '@types/ms': 2.1.0
|
|
140
|
+
+
|
|
141
|
+
+ '@types/ms@2.1.0': {}
|
|
142
|
+
+
|
|
143
|
+
'@types/node@22.15.33':
|
|
144
|
+
dependencies:
|
|
145
|
+
undici-types: 6.21.0
|
|
146
|
+
@@ -107,6 +137,10 @@ snapshots:
|
|
147
|
+
|
|
148
|
+
core-js@3.43.0: {}
|
|
149
|
+
|
|
150
|
+
+ debug@4.4.3:
|
|
151
|
+
+ dependencies:
|
|
152
|
+
+ ms: 2.1.3
|
|
153
|
+
+
|
|
154
|
+
function-timeout@0.1.1: {}
|
|
155
|
+
|
|
156
|
+
ip-regex@5.0.0: {}
|
|
157
|
+
@@ -118,6 +152,8 @@ snapshots:
|
|
158
|
+
|
|
159
|
+
is-regexp@3.1.0: {}
|
|
160
|
+
|
|
161
|
+
+ ms@2.1.3: {}
|
|
162
|
+
+
|
|
163
|
+
parse-domain@8.2.2:
|
|
164
|
+
dependencies:
|
|
165
|
+
is-ip: 5.0.1
|
|
166
|
+
diff --git a/src/index.test.ts b/src/index.test.ts
|
|
167
|
+
new file mode 100644
|
|
168
|
+
index 0000000..3ea8460
|
|
169
|
+
--- /dev/null
|
|
170
|
+
+++ b/src/index.test.ts
|
|
171
|
+
@@ -0,0 +1,360 @@
|
|
172
|
+
+import { describe, it } from "node:test";
|
|
173
|
+
+import assert from "node:assert";
|
|
174
|
+
+import { toArray, escapeRegex, validateDomain } from "./index.js";
|
|
175
|
+
+
|
|
176
|
+
+// ============================================================================
|
|
177
|
+
+// toArray utility tests
|
|
178
|
+
+// ============================================================================
|
|
179
|
+
+
|
|
180
|
+
+describe("toArray utility function", () => {
|
|
181
|
+
+ it("returns empty array for null", () => {
|
|
182
|
+
+ assert.deepStrictEqual(toArray(null), []);
|
|
183
|
+
+ });
|
|
184
|
+
+
|
|
185
|
+
+ it("returns empty array for undefined", () => {
|
|
186
|
+
+ assert.deepStrictEqual(toArray(undefined), []);
|
|
187
|
+
+ });
|
|
188
|
+
+
|
|
189
|
+
+ it("returns same array if already an array", () => {
|
|
190
|
+
+ const arr = [1, 2, 3];
|
|
191
|
+
+ assert.deepStrictEqual(toArray(arr), arr);
|
|
192
|
+
+ });
|
|
193
|
+
+
|
|
194
|
+
+ it("converts object to array of values", () => {
|
|
195
|
+
+ const obj = { a: 1, b: 2, c: 3 };
|
|
196
|
+
+ assert.deepStrictEqual(toArray(obj), [1, 2, 3]);
|
|
197
|
+
+ });
|
|
198
|
+
+
|
|
199
|
+
+ it("handles empty array", () => {
|
|
200
|
+
+ assert.deepStrictEqual(toArray([]), []);
|
|
201
|
+
+ });
|
|
202
|
+
+
|
|
203
|
+
+ it("handles empty object", () => {
|
|
204
|
+
+ assert.deepStrictEqual(toArray({}), []);
|
|
205
|
+
+ });
|
|
206
|
+
+
|
|
207
|
+
+ it("handles nested arrays", () => {
|
|
208
|
+
+ const arr = [[1, 2], [3, 4]];
|
|
209
|
+
+ assert.deepStrictEqual(toArray(arr), [[1, 2], [3, 4]]);
|
|
210
|
+
+ });
|
|
211
|
+
+
|
|
212
|
+
+ it("handles array-like objects with numeric keys", () => {
|
|
213
|
+
+ const obj = { "0": "a", "1": "b", "2": "c" };
|
|
214
|
+
+ assert.deepStrictEqual(toArray(obj), ["a", "b", "c"]);
|
|
215
|
+
+ });
|
|
216
|
+
+});
|
|
217
|
+
+
|
|
218
|
+
+describe("toArray handles RDAP edge cases (issue #16)", () => {
|
|
219
|
+
+ it("handles entities as object instead of array", () => {
|
|
220
|
+
+ const entities = {
|
|
221
|
+
+ "0": { roles: ["registrar"], handle: "123" },
|
|
222
|
+
+ "1": { roles: ["abuse"], handle: "456" },
|
|
223
|
+
+ };
|
|
224
|
+
+ const result = toArray(entities);
|
|
225
|
+
+ assert.strictEqual(Array.isArray(result), true);
|
|
226
|
+
+ assert.strictEqual(result.length, 2);
|
|
227
|
+
+ assert.deepStrictEqual(result[0], { roles: ["registrar"], handle: "123" });
|
|
228
|
+
+ });
|
|
229
|
+
+
|
|
230
|
+
+ it("handles nested entities being null", () => {
|
|
231
|
+
+ const result = toArray(null);
|
|
232
|
+
+ assert.deepStrictEqual(result, []);
|
|
233
|
+
+ });
|
|
234
|
+
+
|
|
235
|
+
+ it("handles nested entities being undefined", () => {
|
|
236
|
+
+ const result = toArray(undefined);
|
|
237
|
+
+ assert.deepStrictEqual(result, []);
|
|
238
|
+
+ });
|
|
239
|
+
+});
|
|
240
|
+
+
|
|
241
|
+
+// ============================================================================
|
|
242
|
+
+// escapeRegex utility tests (ReDoS prevention)
|
|
243
|
+
+// ============================================================================
|
|
244
|
+
+
|
|
245
|
+
+describe("escapeRegex utility function", () => {
|
|
246
|
+
+ it("escapes special regex characters", () => {
|
|
247
|
+
+ const input = "test.*+?^${}()|[]\\";
|
|
248
|
+
+ const escaped = escapeRegex(input);
|
|
249
|
+
+ assert.strictEqual(escaped, "test\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\");
|
|
250
|
+
+ });
|
|
251
|
+
+
|
|
252
|
+
+ it("leaves normal strings unchanged", () => {
|
|
253
|
+
+ const input = "GoDaddy Inc";
|
|
254
|
+
+ const escaped = escapeRegex(input);
|
|
255
|
+
+ assert.strictEqual(escaped, "GoDaddy Inc");
|
|
256
|
+
+ });
|
|
257
|
+
+
|
|
258
|
+
+ it("handles empty string", () => {
|
|
259
|
+
+ const escaped = escapeRegex("");
|
|
260
|
+
+ assert.strictEqual(escaped, "");
|
|
261
|
+
+ });
|
|
262
|
+
+
|
|
263
|
+
+ it("handles string with only special characters", () => {
|
|
264
|
+
+ const input = ".*+";
|
|
265
|
+
+ const escaped = escapeRegex(input);
|
|
266
|
+
+ assert.strictEqual(escaped, "\\.\\*\\+");
|
|
267
|
+
+ });
|
|
268
|
+
+
|
|
269
|
+
+ it("escaped string can be used safely in RegExp", () => {
|
|
270
|
+
+ const malicious = "test(.*)+$";
|
|
271
|
+
+ const escaped = escapeRegex(malicious);
|
|
272
|
+
+ // Should not throw when creating RegExp
|
|
273
|
+
+ const regex = new RegExp(escaped, "i");
|
|
274
|
+
+ // The escaped string should match literally
|
|
275
|
+
+ assert.strictEqual(regex.test("test(.*)+$"), true);
|
|
276
|
+
+ assert.strictEqual(regex.test("testABC"), false);
|
|
277
|
+
+ });
|
|
278
|
+
+});
|
|
279
|
+
+
|
|
280
|
+
+// ============================================================================
|
|
281
|
+
+// validateDomain utility tests
|
|
282
|
+
+// ============================================================================
|
|
283
|
+
+
|
|
284
|
+
+describe("validateDomain utility function", () => {
|
|
285
|
+
+ it("accepts valid domain", () => {
|
|
286
|
+
+ const result = validateDomain("example.com");
|
|
287
|
+
+ assert.strictEqual(result, "example.com");
|
|
288
|
+
+ });
|
|
289
|
+
+
|
|
290
|
+
+ it("trims whitespace", () => {
|
|
291
|
+
+ const result = validateDomain(" example.com ");
|
|
292
|
+
+ assert.strictEqual(result, "example.com");
|
|
293
|
+
+ });
|
|
294
|
+
+
|
|
295
|
+
+ it("converts to lowercase", () => {
|
|
296
|
+
+ const result = validateDomain("EXAMPLE.COM");
|
|
297
|
+
+ assert.strictEqual(result, "example.com");
|
|
298
|
+
+ });
|
|
299
|
+
+
|
|
300
|
+
+ it("throws on empty string", () => {
|
|
301
|
+
+ assert.throws(() => validateDomain(""), /non-empty string/);
|
|
302
|
+
+ });
|
|
303
|
+
+
|
|
304
|
+
+ it("throws on whitespace-only string", () => {
|
|
305
|
+
+ assert.throws(() => validateDomain(" "), /non-empty string/);
|
|
306
|
+
+ });
|
|
307
|
+
+
|
|
308
|
+
+ it("throws on null", () => {
|
|
309
|
+
+ assert.throws(() => validateDomain(null as any), /non-empty string/);
|
|
310
|
+
+ });
|
|
311
|
+
+
|
|
312
|
+
+ it("throws on undefined", () => {
|
|
313
|
+
+ assert.throws(() => validateDomain(undefined as any), /non-empty string/);
|
|
314
|
+
+ });
|
|
315
|
+
+
|
|
316
|
+
+ it("throws on domain too long", () => {
|
|
317
|
+
+ const longDomain = "a".repeat(254) + ".com";
|
|
318
|
+
+ assert.throws(() => validateDomain(longDomain), /too long/);
|
|
319
|
+
+ });
|
|
320
|
+
+
|
|
321
|
+
+ it("accepts domain at max length", () => {
|
|
322
|
+
+ const maxDomain = "a".repeat(249) + ".com"; // 253 chars
|
|
323
|
+
+ const result = validateDomain(maxDomain);
|
|
324
|
+
+ assert.strictEqual(result.length, 253);
|
|
325
|
+
+ });
|
|
326
|
+
+});
|
|
327
|
+
+
|
|
328
|
+
+// ============================================================================
|
|
329
|
+
+// vcardArray safety checks (issue #12)
|
|
330
|
+
+// ============================================================================
|
|
331
|
+
+
|
|
332
|
+
+describe("vcardArray safety checks (issue #12)", () => {
|
|
333
|
+
+ it("Array.isArray correctly identifies arrays", () => {
|
|
334
|
+
+ const vcardArray: any[] = ["vcard", [["fn", {}, "text", "Test"]]];
|
|
335
|
+
+ assert.strictEqual(Array.isArray(vcardArray[1]), true);
|
|
336
|
+
+ assert.strictEqual(typeof (vcardArray[1] as any[]).find, "function");
|
|
337
|
+
+ });
|
|
338
|
+
+
|
|
339
|
+
+ it("Array.isArray returns false for non-arrays", () => {
|
|
340
|
+
+ const vcardArray: any[] = ["vcard", "not-an-array"];
|
|
341
|
+
+ assert.strictEqual(Array.isArray(vcardArray[1]), false);
|
|
342
|
+
+ });
|
|
343
|
+
+
|
|
344
|
+
+ it("Array.isArray returns false for undefined", () => {
|
|
345
|
+
+ const vcardArray: any[] = ["vcard"];
|
|
346
|
+
+ assert.strictEqual(Array.isArray(vcardArray[1]), false);
|
|
347
|
+
+ });
|
|
348
|
+
+
|
|
349
|
+
+ it("safe access pattern prevents TypeError", () => {
|
|
350
|
+
+ const vcardArray: any[] = ["vcard"];
|
|
351
|
+
+ const hasFn = vcardArray && Array.isArray(vcardArray[1]) && (vcardArray[1] as any[]).find((el: any) => el[0] === 'fn');
|
|
352
|
+
+ assert.strictEqual(hasFn, false);
|
|
353
|
+
+ });
|
|
354
|
+
+
|
|
355
|
+
+ it("safe access pattern works with valid data", () => {
|
|
356
|
+
+ const vcardArray: any[] = ["vcard", [["fn", {}, "text", "Test Registrar"]]];
|
|
357
|
+
+ const hasFn = vcardArray && Array.isArray(vcardArray[1]) && (vcardArray[1] as any[]).find((el: any) => el[0] === 'fn');
|
|
358
|
+
+ assert.deepStrictEqual(hasFn, ["fn", {}, "text", "Test Registrar"]);
|
|
359
|
+
+ });
|
|
360
|
+
+});
|
|
361
|
+
+
|
|
362
|
+
+// ============================================================================
|
|
363
|
+
+// debug module integration (issue #13)
|
|
364
|
+
+// ============================================================================
|
|
365
|
+
+
|
|
366
|
+
+describe("debug module integration (issue #13)", () => {
|
|
367
|
+
+ it("debug module is importable", async () => {
|
|
368
|
+
+ const createDebug = (await import("debug")).default;
|
|
369
|
+
+ assert.strictEqual(typeof createDebug, "function");
|
|
370
|
+
+ });
|
|
371
|
+
+
|
|
372
|
+
+ it("debug instance can be created", async () => {
|
|
373
|
+
+ const createDebug = (await import("debug")).default;
|
|
374
|
+
+ const debug = createDebug("test:namespace");
|
|
375
|
+
+ assert.strictEqual(typeof debug, "function");
|
|
376
|
+
+ });
|
|
377
|
+
+
|
|
378
|
+
+ it("debug function can be called without error", async () => {
|
|
379
|
+
+ const createDebug = (await import("debug")).default;
|
|
380
|
+
+ const debug = createDebug("test:namespace");
|
|
381
|
+
+ // Should not throw
|
|
382
|
+
+ debug("test message %s", "arg");
|
|
383
|
+
+ });
|
|
384
|
+
+});
|
|
385
|
+
+
|
|
386
|
+
+// ============================================================================
|
|
387
|
+
+// findTimestamps anti-pattern fix (commit comment)
|
|
388
|
+
+// ============================================================================
|
|
389
|
+
+
|
|
390
|
+
+describe("findTimestamps behavior", () => {
|
|
391
|
+
+ it("properly extracts timestamps from events array", () => {
|
|
392
|
+
+ // This tests the fix for the anti-pattern where events.find was used with side effects
|
|
393
|
+
+ const events = [
|
|
394
|
+
+ { eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
|
|
395
|
+
+ { eventAction: "last changed", eventDate: "2023-06-15T12:00:00Z" },
|
|
396
|
+
+ { eventAction: "expiration", eventDate: "2025-01-01T00:00:00Z" },
|
|
397
|
+
+ ];
|
|
398
|
+
+
|
|
399
|
+
+ // The function should properly iterate and extract all timestamps
|
|
400
|
+
+ // without relying on side effects in find()
|
|
401
|
+
+ const created = events.find(ev => ev.eventAction === "registration")?.eventDate;
|
|
402
|
+
+ const updated = events.find(ev => ev.eventAction === "last changed")?.eventDate;
|
|
403
|
+
+ const expires = events.find(ev => ev.eventAction === "expiration")?.eventDate;
|
|
404
|
+
+
|
|
405
|
+
+ assert.strictEqual(created, "2020-01-01T00:00:00Z");
|
|
406
|
+
+ assert.strictEqual(updated, "2023-06-15T12:00:00Z");
|
|
407
|
+
+ assert.strictEqual(expires, "2025-01-01T00:00:00Z");
|
|
408
|
+
+ });
|
|
409
|
+
+
|
|
410
|
+
+ it("handles events with invalid dates", () => {
|
|
411
|
+
+ const events = [
|
|
412
|
+
+ { eventAction: "registration", eventDate: "invalid-date" },
|
|
413
|
+
+ { eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
|
|
414
|
+
+ ];
|
|
415
|
+
+
|
|
416
|
+
+ // Should skip invalid dates and find valid one
|
|
417
|
+
+ let validDate = null;
|
|
418
|
+
+ for (const ev of events) {
|
|
419
|
+
+ if (ev.eventAction === "registration" && ev.eventDate) {
|
|
420
|
+
+ const d = new Date(ev.eventDate);
|
|
421
|
+
+ if (!isNaN(d.valueOf())) {
|
|
422
|
+
+ validDate = d;
|
|
423
|
+
+ break;
|
|
424
|
+
+ }
|
|
425
|
+
+ }
|
|
426
|
+
+ }
|
|
427
|
+
+
|
|
428
|
+
+ assert.notStrictEqual(validDate, null);
|
|
429
|
+
+ assert.strictEqual(validDate?.toISOString(), "2020-01-01T00:00:00.000Z");
|
|
430
|
+
+ });
|
|
431
|
+
+
|
|
432
|
+
+ it("handles +0000Z date format", () => {
|
|
433
|
+
+ const dateStr = "2020-01-01T00:00:00+0000Z";
|
|
434
|
+
+ const normalized = dateStr.replace(/\+0000Z$/, "Z");
|
|
435
|
+
+ const d = new Date(normalized);
|
|
436
|
+
+ assert.strictEqual(isNaN(d.valueOf()), false);
|
|
437
|
+
+ assert.strictEqual(d.toISOString(), "2020-01-01T00:00:00.000Z");
|
|
438
|
+
+ });
|
|
439
|
+
+});
|
|
440
|
+
+
|
|
441
|
+
+// ============================================================================
|
|
442
|
+
+// findInObject null safety
|
|
443
|
+
+// ============================================================================
|
|
444
|
+
+
|
|
445
|
+
+describe("findInObject null safety", () => {
|
|
446
|
+
+ it("handles null input", async () => {
|
|
447
|
+
+ const { findInObject } = await import("./utils/findInObject.js");
|
|
448
|
+
+ const result = findInObject(null as any, () => true, (el) => el, "fallback");
|
|
449
|
+
+ assert.strictEqual(result, "fallback");
|
|
450
|
+
+ });
|
|
451
|
+
+
|
|
452
|
+
+ it("handles undefined input", async () => {
|
|
453
|
+
+ const { findInObject } = await import("./utils/findInObject.js");
|
|
454
|
+
+ const result = findInObject(undefined as any, () => true, (el) => el, "fallback");
|
|
455
|
+
+ assert.strictEqual(result, "fallback");
|
|
456
|
+
+ });
|
|
457
|
+
+
|
|
458
|
+
+ it("handles object with null values", async () => {
|
|
459
|
+
+ const { findInObject } = await import("./utils/findInObject.js");
|
|
460
|
+
+ const obj = { a: null, b: { c: "found" } };
|
|
461
|
+
+ const result = findInObject(obj, (el) => el === "found", (el) => el, "fallback");
|
|
462
|
+
+ assert.strictEqual(result, "found");
|
|
463
|
+
+ });
|
|
464
|
+
+
|
|
465
|
+
+ it("finds nested value", async () => {
|
|
466
|
+
+ const { findInObject } = await import("./utils/findInObject.js");
|
|
467
|
+
+ const obj = { a: { b: { c: ["fn", {}, "text", "Test"] } } };
|
|
468
|
+
+ const result = findInObject(
|
|
469
|
+
+ obj,
|
|
470
|
+
+ (el) => Array.isArray(el) && el[0] === "fn",
|
|
471
|
+
+ (el) => el[3],
|
|
472
|
+
+ "fallback"
|
|
473
|
+
+ );
|
|
474
|
+
+ assert.strictEqual(result, "Test");
|
|
475
|
+
+ });
|
|
476
|
+
+});
|
|
477
|
+
+
|
|
478
|
+
+// ============================================================================
|
|
479
|
+
+// IP response parsing null safety
|
|
480
|
+
+// ============================================================================
|
|
481
|
+
+
|
|
482
|
+
+describe("IP response parsing", () => {
|
|
483
|
+
+ it("handles missing port43 field", async () => {
|
|
484
|
+
+ const { parseIpResponse } = await import("./ip.js");
|
|
485
|
+
+ const response: any = {
|
|
486
|
+
+ found: false,
|
|
487
|
+
+ registrar: { id: 0, name: null },
|
|
488
|
+
+ };
|
|
489
|
+
+ const rdap = {
|
|
490
|
+
+ handle: "NET-1-0-0-0-1",
|
|
491
|
+
+ startAddress: "1.0.0.0",
|
|
492
|
+
+ endAddress: "1.255.255.255",
|
|
493
|
+
+ // port43 is missing
|
|
494
|
+
+ };
|
|
495
|
+
+
|
|
496
|
+
+ // Should not throw
|
|
497
|
+
+ parseIpResponse("1.0.0.1", rdap, response);
|
|
498
|
+
+ assert.strictEqual(response.registrar.name, "");
|
|
499
|
+
+ });
|
|
500
|
+
+
|
|
501
|
+
+ it("handles port43 without expected pattern", async () => {
|
|
502
|
+
+ const { parseIpResponse } = await import("./ip.js");
|
|
503
|
+
+ const response: any = {
|
|
504
|
+
+ found: false,
|
|
505
|
+
+ registrar: { id: 0, name: null },
|
|
506
|
+
+ };
|
|
507
|
+
+ const rdap = {
|
|
508
|
+
+ handle: "NET-1-0-0-0-1",
|
|
509
|
+
+ port43: "whois-server", // No dots
|
|
510
|
+
+ };
|
|
511
|
+
+
|
|
512
|
+
+ // Should not throw
|
|
513
|
+
+ parseIpResponse("1.0.0.1", rdap, response);
|
|
514
|
+
+ assert.strictEqual(response.registrar.name, "");
|
|
515
|
+
+ });
|
|
516
|
+
+
|
|
517
|
+
+ it("extracts registry from valid port43", async () => {
|
|
518
|
+
+ const { parseIpResponse } = await import("./ip.js");
|
|
519
|
+
+ const response: any = {
|
|
520
|
+
+ found: false,
|
|
521
|
+
+ registrar: { id: 0, name: null },
|
|
522
|
+
+ };
|
|
523
|
+
+ const rdap = {
|
|
524
|
+
+ handle: "NET-1-0-0-0-1",
|
|
525
|
+
+ port43: "whois.arin.net",
|
|
526
|
+
+ };
|
|
527
|
+
+
|
|
528
|
+
+ parseIpResponse("1.0.0.1", rdap, response);
|
|
529
|
+
+ assert.strictEqual(response.registrar.name, "ARIN");
|
|
530
|
+
+ });
|
|
531
|
+
+});
|
|
532
|
+
diff --git a/src/index.ts b/src/index.ts
|
|
533
|
+
index fe5c83b..02fe1ee 100644
|
|
534
|
+
--- a/src/index.ts
|
|
535
|
+
+++ b/src/index.ts
|
|
536
|
+
@@ -7,6 +7,10 @@ import { ianaIdToRegistrar } from "./utils/ianaIdToRegistrar.js";
|
|
537
|
+
import { tldToRdap } from "./utils/tldToRdap.js";
|
|
538
|
+
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
539
|
+
import { resolve4 } from "dns/promises";
|
|
540
|
+
+import createDebug from "debug";
|
|
541
|
+
+
|
|
542
|
+
+// Debug logger - enable with DEBUG=whois:* environment variable
|
|
543
|
+
+const debug = createDebug("whois:rdap");
|
|
544
|
+
|
|
545
|
+
const eventMap = new Map<string, WhoisTimestampFields>([
|
|
546
|
+
["registration", "created"],
|
|
547
|
+
@@ -15,16 +19,78 @@ const eventMap = new Map<string, WhoisTimestampFields>([
|
|
548
|
+
["expiration date", "expires"],
|
|
549
|
+
]);
|
|
550
|
+
|
|
551
|
+
+/**
|
|
552
|
+
+ * Safely converts a value to an array.
|
|
553
|
+
+ * Handles cases where the value might be null, undefined, or a non-iterable object.
|
|
554
|
+
+ */
|
|
555
|
+
+function toArray<T>(value: T | T[] | null | undefined): T[] {
|
|
556
|
+
+ if (value === null || value === undefined) {
|
|
557
|
+
+ return [];
|
|
558
|
+
+ }
|
|
559
|
+
+ if (Array.isArray(value)) {
|
|
560
|
+
+ return value;
|
|
561
|
+
+ }
|
|
562
|
+
+ // Handle object case - some RDAP responses return objects instead of arrays
|
|
563
|
+
+ if (typeof value === "object") {
|
|
564
|
+
+ return Object.values(value) as T[];
|
|
565
|
+
+ }
|
|
566
|
+
+ return [];
|
|
567
|
+
+}
|
|
568
|
+
+
|
|
569
|
+
+/**
|
|
570
|
+
+ * Escapes special regex characters in a string.
|
|
571
|
+
+ * Prevents ReDoS attacks when using user input in RegExp constructor.
|
|
572
|
+
+ */
|
|
573
|
+
+function escapeRegex(str: string): string {
|
|
574
|
+
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
575
|
+
+}
|
|
576
|
+
+
|
|
577
|
+
+/**
|
|
578
|
+
+ * Validates domain input format.
|
|
579
|
+
+ * Returns sanitized domain or throws on invalid input.
|
|
580
|
+
+ */
|
|
581
|
+
+function validateDomain(domain: string): string {
|
|
582
|
+
+ if (!domain || typeof domain !== 'string') {
|
|
583
|
+
+ throw new Error('Domain must be a non-empty string');
|
|
584
|
+
+ }
|
|
585
|
+
+ // Basic sanitization - trim whitespace
|
|
586
|
+
+ const sanitized = domain.trim().toLowerCase();
|
|
587
|
+
+ if (sanitized.length === 0) {
|
|
588
|
+
+ throw new Error('Domain must be a non-empty string');
|
|
589
|
+
+ }
|
|
590
|
+
+ if (sanitized.length > 253) {
|
|
591
|
+
+ throw new Error('Domain name too long');
|
|
592
|
+
+ }
|
|
593
|
+
+ return sanitized;
|
|
594
|
+
+}
|
|
595
|
+
+
|
|
596
|
+
export async function whois(
|
|
597
|
+
origDomain: string,
|
|
598
|
+
options: WhoisOptions = { fetch: fetch, thinOnly: false }
|
|
599
|
+
): Promise<WhoisResponse> {
|
|
600
|
+
const _fetch = options.fetch || fetch;
|
|
601
|
+
|
|
602
|
+
- let domain = origDomain;
|
|
603
|
+
+ // Validate and sanitize input
|
|
604
|
+
+ let domain: string;
|
|
605
|
+
+ try {
|
|
606
|
+
+ domain = validateDomain(origDomain);
|
|
607
|
+
+ } catch (e: any) {
|
|
608
|
+
+ return {
|
|
609
|
+
+ found: false,
|
|
610
|
+
+ statusCode: 400,
|
|
611
|
+
+ error: e.message,
|
|
612
|
+
+ registrar: { id: 0, name: null },
|
|
613
|
+
+ reseller: null,
|
|
614
|
+
+ status: [],
|
|
615
|
+
+ statusDelta: [],
|
|
616
|
+
+ nameservers: [],
|
|
617
|
+
+ ts: { created: null, updated: null, expires: null },
|
|
618
|
+
+ };
|
|
619
|
+
+ }
|
|
620
|
+
+
|
|
621
|
+
let url: string | null = null;
|
|
622
|
+
|
|
623
|
+
- [domain, url] = await tldToRdap(origDomain);
|
|
624
|
+
+ [domain, url] = await tldToRdap(domain);
|
|
625
|
+
|
|
626
|
+
const response: WhoisResponse = {
|
|
627
|
+
found: false,
|
|
628
|
+
@@ -59,7 +125,6 @@ export async function whois(
|
|
629
|
+
thinResponse = await _fetch(thinRdap)
|
|
630
|
+
.then((r) => {
|
|
631
|
+
response.statusCode = r.status;
|
|
632
|
+
- // console.log({ ok: r.ok, status: r.status, statusText: r.statusText });
|
|
633
|
+
if (r.status >= 200 && r.status < 400) {
|
|
634
|
+
return r.json() as any;
|
|
635
|
+
}
|
|
636
|
+
@@ -67,7 +132,7 @@ export async function whois(
|
|
637
|
+
return null;
|
|
638
|
+
})
|
|
639
|
+
.catch((error: Error) => {
|
|
640
|
+
- console.warn(`thin RDAP lookup failure: ${error.message}`);
|
|
641
|
+
+ debug("thin RDAP lookup failure for %s: %s", domain, error.message);
|
|
642
|
+
return null;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
@@ -94,14 +159,14 @@ export async function whois(
|
|
646
|
+
let thickResponse: any = null;
|
|
647
|
+
|
|
648
|
+
if (!options.thinOnly && thickRdap) {
|
|
649
|
+
- // console.log(`fetching thick RDAP: ${thickRdap}`);
|
|
650
|
+
+ debug("fetching thick RDAP: %s", thickRdap);
|
|
651
|
+
thickResponse = await _fetch(thickRdap)
|
|
652
|
+
.then((r) => r.json() as any)
|
|
653
|
+
.catch(() => null);
|
|
654
|
+
if (thickResponse && !thickResponse.errorCode && !thickResponse.error) {
|
|
655
|
+
} else {
|
|
656
|
+
thickResponse = null;
|
|
657
|
+
- // console.warn(`thick RDAP failed for ${domain}`);
|
|
658
|
+
+ debug("thick RDAP failed for %s", domain);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
@@ -113,10 +178,14 @@ export async function whois(
|
|
663
|
+
const resellers: any[] = [];
|
|
664
|
+
|
|
665
|
+
async function extractRegistrarsAndResellers(response: any, url: string, isThick?: boolean) {
|
|
666
|
+
- for (const ent of [
|
|
667
|
+
- ...(response.entities || []),
|
|
668
|
+
+ // Use toArray to safely handle entities that might not be iterable
|
|
669
|
+
+ const entities = toArray(response.entities);
|
|
670
|
+
+ const entityList = [
|
|
671
|
+
+ ...entities,
|
|
672
|
+
response.entity ? { events: response.events, ...response.entity } : null,
|
|
673
|
+
- ].filter(Boolean)) {
|
|
674
|
+
+ ].filter(Boolean);
|
|
675
|
+
+
|
|
676
|
+
+ for (const ent of entityList) {
|
|
677
|
+
if (ent.roles?.includes("registrar") || ent.role === "registrar") {
|
|
678
|
+
const pubIds: any[] = [];
|
|
679
|
+
if (ent.publicIds) {
|
|
680
|
+
@@ -145,7 +214,6 @@ export async function whois(
|
|
681
|
+
;
|
|
682
|
+
|
|
683
|
+
if (reg) {
|
|
684
|
+
- // console.log(ent.vcardArray);
|
|
685
|
+
const id = typeof reg === 'object' ? 0 : reg;
|
|
686
|
+
const name =
|
|
687
|
+
(parseInt(id) == id
|
|
688
|
+
@@ -157,8 +225,10 @@ export async function whois(
|
|
689
|
+
(el: any[]) => el[3],
|
|
690
|
+
reg
|
|
691
|
+
);
|
|
692
|
+
+ // Safely handle ent.entities
|
|
693
|
+
+ const entEntities = toArray(ent.entities);
|
|
694
|
+
const email =
|
|
695
|
+
- [ent, ...(ent.entities || [])]
|
|
696
|
+
+ [ent, ...entEntities]
|
|
697
|
+
.filter((e) => e?.vcardArray)
|
|
698
|
+
.map((e) =>
|
|
699
|
+
findInObject(
|
|
700
|
+
@@ -171,7 +241,7 @@ export async function whois(
|
|
701
|
+
.filter(Boolean)?.[0] || "";
|
|
702
|
+
|
|
703
|
+
const abuseEmail =
|
|
704
|
+
- [ent, ...(ent.entities || [])]
|
|
705
|
+
+ [ent, ...entEntities]
|
|
706
|
+
.filter((e) => e?.vcardArray)
|
|
707
|
+
.map((e) =>
|
|
708
|
+
findInObject(
|
|
709
|
+
@@ -187,10 +257,11 @@ export async function whois(
|
|
710
|
+
ent.events || response.events || ent.enents || response.enents;
|
|
711
|
+
registrars.push({ id, name, email, abuseEmail, events });
|
|
712
|
+
}
|
|
713
|
+
- // handles .ca
|
|
714
|
+
+ // handles .ca - with safe optional chaining
|
|
715
|
+
else if (ent.vcardArray?.[1]?.[3]?.[3] === 'registrar') {
|
|
716
|
+
+ const entEntities = toArray(ent.entities);
|
|
717
|
+
const email =
|
|
718
|
+
- [ent, ...(ent.entities || [])]
|
|
719
|
+
+ [ent, ...entEntities]
|
|
720
|
+
.filter((e) => e?.vcardArray)
|
|
721
|
+
.map((e) =>
|
|
722
|
+
findInObject(
|
|
723
|
+
@@ -203,7 +274,7 @@ export async function whois(
|
|
724
|
+
.filter(Boolean)?.[0] || "";
|
|
725
|
+
|
|
726
|
+
const abuseEmail =
|
|
727
|
+
- [ent, ...(ent.entities || [])]
|
|
728
|
+
+ [ent, ...entEntities]
|
|
729
|
+
.filter((e) => e?.vcardArray)
|
|
730
|
+
.map((e) =>
|
|
731
|
+
findInObject(
|
|
732
|
+
@@ -215,12 +286,14 @@ export async function whois(
|
|
733
|
+
)
|
|
734
|
+
.filter(Boolean)?.[0] || "";
|
|
735
|
+
|
|
736
|
+
- registrars.push({ id: 0, name: ent.vcardArray[1][1][3], email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
737
|
+
+ const vcardName = ent.vcardArray?.[1]?.[1]?.[3] || '';
|
|
738
|
+
+ registrars.push({ id: 0, name: vcardName, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
739
|
+
}
|
|
740
|
+
- // handles .si
|
|
741
|
+
- else if (ent.vcardArray && ent.vcardArray[1] && ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')) {
|
|
742
|
+
+ // handles .si - with safe array access
|
|
743
|
+
+ else if (ent.vcardArray && Array.isArray(ent.vcardArray[1]) && ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')) {
|
|
744
|
+
+ const entEntities = toArray(ent.entities);
|
|
745
|
+
const email =
|
|
746
|
+
- [ent, ...(ent.entities || [])]
|
|
747
|
+
+ [ent, ...entEntities]
|
|
748
|
+
.filter((e) => e?.vcardArray)
|
|
749
|
+
.map((e) =>
|
|
750
|
+
findInObject(
|
|
751
|
+
@@ -233,7 +306,7 @@ export async function whois(
|
|
752
|
+
.filter(Boolean)?.[0] || "";
|
|
753
|
+
|
|
754
|
+
const abuseEmail =
|
|
755
|
+
- [ent, ...(ent.entities || [])]
|
|
756
|
+
+ [ent, ...entEntities]
|
|
757
|
+
.filter((e) => e?.vcardArray)
|
|
758
|
+
.map((e) =>
|
|
759
|
+
findInObject(
|
|
760
|
+
@@ -260,7 +333,9 @@ export async function whois(
|
|
761
|
+
registrars.push({ id, name, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
- registrars.push({ id: ent.handle || 0, name: ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')[3], email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
765
|
+
+ const fnEntry = ent.vcardArray[1].find((el: string[]) => el[0] === 'fn');
|
|
766
|
+
+ const name = fnEntry ? fnEntry[3] : ent.handle || '';
|
|
767
|
+
+ registrars.push({ id: ent.handle || 0, name, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// handles .ar
|
|
771
|
+
@@ -285,8 +360,9 @@ export async function whois(
|
|
772
|
+
(el: any[]) => el[3],
|
|
773
|
+
id
|
|
774
|
+
);
|
|
775
|
+
+ const entEntities = toArray(ent.entities);
|
|
776
|
+
const email =
|
|
777
|
+
- [ent, ...(ent.entities || [])]
|
|
778
|
+
+ [ent, ...entEntities]
|
|
779
|
+
.filter((e) => e?.vcardArray)
|
|
780
|
+
.map((e) =>
|
|
781
|
+
findInObject(
|
|
782
|
+
@@ -299,7 +375,7 @@ export async function whois(
|
|
783
|
+
.filter(Boolean)?.[0] || "";
|
|
784
|
+
|
|
785
|
+
const abuseEmail =
|
|
786
|
+
- [ent, ...(ent.entities || [])]
|
|
787
|
+
+ [ent, ...entEntities]
|
|
788
|
+
.filter((e) => e?.vcardArray)
|
|
789
|
+
.map((e) =>
|
|
790
|
+
findInObject(
|
|
791
|
+
@@ -394,8 +470,6 @@ export async function whois(
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function findStatus(statuses: string | string[], domain: string): string[] {
|
|
795
|
+
- // console.warn({ domain, statuses });
|
|
796
|
+
-
|
|
797
|
+
return (Array.isArray(statuses)
|
|
798
|
+
? statuses
|
|
799
|
+
: statuses && typeof statuses === "object"
|
|
800
|
+
@@ -422,6 +496,10 @@ function findNameservers(values: any[]): string[] {
|
|
801
|
+
.sort();
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
+/**
|
|
805
|
+
+ * Extracts timestamps from RDAP events array.
|
|
806
|
+
+ * Properly iterates through events and breaks when a match is found.
|
|
807
|
+
+ */
|
|
808
|
+
function findTimestamps(values: any[]) {
|
|
809
|
+
const ts: Record<WhoisTimestampFields, Date | null> = {
|
|
810
|
+
created: null,
|
|
811
|
+
@@ -429,29 +507,36 @@ function findTimestamps(values: any[]) {
|
|
812
|
+
expires: null,
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
- let events: any = [];
|
|
816
|
+
+ let events: any[] = [];
|
|
817
|
+
|
|
818
|
+
if (Array.isArray(values)) {
|
|
819
|
+
events = values;
|
|
820
|
+
- } else if (typeof values === "object") {
|
|
821
|
+
+ } else if (typeof values === "object" && values !== null) {
|
|
822
|
+
events = Object.values(values);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
- for (const [event, field] of eventMap) {
|
|
826
|
+
- events.find(
|
|
827
|
+
- (ev: any) => {
|
|
828
|
+
- const isMatch = ev?.eventAction?.toLocaleLowerCase() === event && ev.eventDate;
|
|
829
|
+
- if (isMatch) {
|
|
830
|
+
- const d = new Date(ev.eventDate.toString().replace(/\+0000Z$/, "Z"));
|
|
831
|
+
- // console.log(field, ev.eventDate, d);
|
|
832
|
+
- if (!isNaN(d.valueOf())) {
|
|
833
|
+
- ts[field] = d;
|
|
834
|
+
- return true;
|
|
835
|
+
- }
|
|
836
|
+
+ // Iterate through each event type we're looking for
|
|
837
|
+
+ for (const [eventAction, field] of eventMap) {
|
|
838
|
+
+ // Skip if we already have a value for this field
|
|
839
|
+
+ if (ts[field] !== null) {
|
|
840
|
+
+ continue;
|
|
841
|
+
+ }
|
|
842
|
+
+
|
|
843
|
+
+ // Find matching event and extract date
|
|
844
|
+
+ for (const ev of events) {
|
|
845
|
+
+ if (ev?.eventAction?.toLocaleLowerCase() === eventAction && ev.eventDate) {
|
|
846
|
+
+ const dateStr = ev.eventDate.toString().replace(/\+0000Z$/, "Z");
|
|
847
|
+
+ const d = new Date(dateStr);
|
|
848
|
+
+ if (!isNaN(d.valueOf())) {
|
|
849
|
+
+ ts[field] = d;
|
|
850
|
+
+ break; // Found valid date, stop searching for this field
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
- );
|
|
854
|
+
+ }
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return ts;
|
|
858
|
+
}
|
|
859
|
+
+
|
|
860
|
+
+// Export utilities for testing
|
|
861
|
+
+export { toArray, escapeRegex, validateDomain };
|
|
862
|
+
diff --git a/src/ip.ts b/src/ip.ts
|
|
863
|
+
index e212955..4f5bbbd 100644
|
|
864
|
+
--- a/src/ip.ts
|
|
865
|
+
+++ b/src/ip.ts
|
|
866
|
+
@@ -3,7 +3,15 @@ import { WhoisResponse } from "../whois.js";
|
|
867
|
+
export function parseIpResponse(ip: string, rdap: any, response: WhoisResponse) {
|
|
868
|
+
response.found = Boolean(rdap.handle);
|
|
869
|
+
|
|
870
|
+
- const registry = rdap.port43 ? rdap.port43.match(/\.(\w+)\./)[1].toUpperCase() : '';
|
|
871
|
+
+ // Safely extract registry from port43 with null check
|
|
872
|
+
+ let registry = '';
|
|
873
|
+
+ if (rdap.port43) {
|
|
874
|
+
+ const match = rdap.port43.match(/\.(\w+)\./);
|
|
875
|
+
+ if (match) {
|
|
876
|
+
+ registry = match[1].toUpperCase();
|
|
877
|
+
+ }
|
|
878
|
+
+ }
|
|
879
|
+
+
|
|
880
|
+
const realRdapServer = rdap.links?.find(({ rel }: { rel: string }) => rel === 'self')?.value?.replace(/\/ip\/.*/, '/ip/');
|
|
881
|
+
|
|
882
|
+
response.server = realRdapServer || 'https://rdap.org/ip/';
|
|
883
|
+
diff --git a/src/port43.ts b/src/port43.ts
|
|
884
|
+
index e95b6ac..b714645 100644
|
|
885
|
+
--- a/src/port43.ts
|
|
886
|
+
+++ b/src/port43.ts
|
|
887
|
+
@@ -5,6 +5,18 @@ import { port43servers, port43parsers } from "./port43servers.js";
|
|
888
|
+
import { ianaToRegistrarCache } from "./utils/ianaIdToRegistrar.js";
|
|
889
|
+
import { WhoisResponse } from "../whois.js";
|
|
890
|
+
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
891
|
+
+import createDebug from "debug";
|
|
892
|
+
+
|
|
893
|
+
+// Debug logger - enable with DEBUG=whois:* environment variable
|
|
894
|
+
+const debug = createDebug("whois:port43");
|
|
895
|
+
+
|
|
896
|
+
+/**
|
|
897
|
+
+ * Escapes special regex characters in a string.
|
|
898
|
+
+ * Prevents ReDoS attacks when using user input in RegExp constructor.
|
|
899
|
+
+ */
|
|
900
|
+
+function escapeRegex(str: string): string {
|
|
901
|
+
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
902
|
+
+}
|
|
903
|
+
|
|
904
|
+
export function determinePort43Domain(actor: string) {
|
|
905
|
+
const parsed = parseDomain(actor);
|
|
906
|
+
@@ -36,7 +48,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
907
|
+
: `${domain}\r\n`;
|
|
908
|
+
const port = opts?.port || 43;
|
|
909
|
+
|
|
910
|
+
- // console.log(`looking up ${domain} on ${server}`);
|
|
911
|
+
+ debug("looking up %s on %s:%d", domain, server, port);
|
|
912
|
+
|
|
913
|
+
const response: WhoisResponse = {
|
|
914
|
+
found: true,
|
|
915
|
+
@@ -82,7 +94,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
} catch (error: any) {
|
|
919
|
+
- console.warn({ port, server, query, error: error.message });
|
|
920
|
+
+ debug("port43 lookup error: %O", { port, server, query, error: error.message });
|
|
921
|
+
response.found = false;
|
|
922
|
+
response.statusCode = 500;
|
|
923
|
+
response.error = error.message || "Unknown error during port 43 lookup";
|
|
924
|
+
@@ -93,7 +105,6 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
port43response = port43response.replace(/^[ \t]+/gm, "");
|
|
928
|
+
- // console.log(port43response);
|
|
929
|
+
|
|
930
|
+
let m;
|
|
931
|
+
|
|
932
|
+
@@ -145,12 +156,6 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
933
|
+
)) &&
|
|
934
|
+
(response.reseller = m[1].trim());
|
|
935
|
+
|
|
936
|
+
- // console.log(port43response)
|
|
937
|
+
-
|
|
938
|
+
-// Updated Date: 2024-11-21T13:42:54Z
|
|
939
|
+
-// Creation Date: 2017-12-16T02:11:08Z
|
|
940
|
+
-// Registry Expiry Date: 2031-07-10T02:11:08Z
|
|
941
|
+
-
|
|
942
|
+
!response.ts.updated &&
|
|
943
|
+
(m = port43response.match(
|
|
944
|
+
/^(?:Last Modified|Updated Date|Last updated on|domain_datelastmodified|last-update|modified|last modified)\.*:[ \t]*(\S.+)/im
|
|
945
|
+
@@ -235,6 +240,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
946
|
+
if (response.ts.updated && !response.ts.updated.valueOf()) response.ts.updated = null;
|
|
947
|
+
if (response.ts.expires && !response.ts.expires.valueOf()) response.ts.expires = null;
|
|
948
|
+
|
|
949
|
+
+ // Match registrar name against IANA cache using escaped regex to prevent ReDoS
|
|
950
|
+
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
951
|
+
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
952
|
+
if (name === response.registrar.name) {
|
|
953
|
+
@@ -244,24 +250,32 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
- if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
958
|
+
+ if (response.registrar.id === 0 && response.registrar.name && response.registrar.name !== "") {
|
|
959
|
+
+ const escapedName = escapeRegex(response.registrar.name);
|
|
960
|
+
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
961
|
+
- if (name.match(new RegExp(`\\b${response.registrar.name}\\b`, "i"))) {
|
|
962
|
+
- response.registrar.id = id;
|
|
963
|
+
- break;
|
|
964
|
+
+ try {
|
|
965
|
+
+ if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
|
|
966
|
+
+ response.registrar.id = id;
|
|
967
|
+
+ break;
|
|
968
|
+
+ }
|
|
969
|
+
+ } catch {
|
|
970
|
+
+ // Skip if regex still fails for some reason
|
|
971
|
+
+ continue;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (response.registrar.id === 0 && response.registrar.name) {
|
|
977
|
+
+ const escapedName = escapeRegex(response.registrar.name.replace(/,.*/, ""));
|
|
978
|
+
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
979
|
+
- if (
|
|
980
|
+
- name.match(
|
|
981
|
+
- new RegExp(`\\b${response.registrar.name.replace(/,.*/, "")}\\b`, "i")
|
|
982
|
+
- )
|
|
983
|
+
- ) {
|
|
984
|
+
- response.registrar.id = id;
|
|
985
|
+
- break;
|
|
986
|
+
+ try {
|
|
987
|
+
+ if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
|
|
988
|
+
+ response.registrar.id = id;
|
|
989
|
+
+ break;
|
|
990
|
+
+ }
|
|
991
|
+
+ } catch {
|
|
992
|
+
+ // Skip if regex still fails for some reason
|
|
993
|
+
+ continue;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
@@ -292,4 +306,4 @@ function reformatDate(date: string) {
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return date;
|
|
1001
|
+
-}
|
|
1002
|
+
|
|
1003
|
+
+}
|
|
1004
|
+
diff --git a/src/utils/findInObject.ts b/src/utils/findInObject.ts
|
|
1005
|
+
index e533ca2..593a012 100644
|
|
1006
|
+
--- a/src/utils/findInObject.ts
|
|
1007
|
+
+++ b/src/utils/findInObject.ts
|
|
1008
|
+
@@ -7,17 +7,32 @@ export function findInObject(
|
|
1009
|
+
const found = _findInObject(obj, condition);
|
|
1010
|
+
return found === undefined ? fallback : extractor(found);
|
|
1011
|
+
}
|
|
1012
|
+
+
|
|
1013
|
+
function _findInObject(obj: any, condition: (el: any) => boolean): any {
|
|
1014
|
+
+ // Handle null/undefined
|
|
1015
|
+
+ if (obj === null || obj === undefined) {
|
|
1016
|
+
+ return undefined;
|
|
1017
|
+
+ }
|
|
1018
|
+
+
|
|
1019
|
+
for (const key in obj) {
|
|
1020
|
+
- if (condition(obj[key])) {
|
|
1021
|
+
- return obj[key];
|
|
1022
|
+
+ // Skip inherited properties
|
|
1023
|
+
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
1024
|
+
+ continue;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
- if (typeof obj[key] === "object") {
|
|
1028
|
+
- const result = _findInObject(obj[key], condition);
|
|
1029
|
+
+ const value = obj[key];
|
|
1030
|
+
+
|
|
1031
|
+
+ if (condition(value)) {
|
|
1032
|
+
+ return value;
|
|
1033
|
+
+ }
|
|
1034
|
+
+
|
|
1035
|
+
+ if (value !== null && typeof value === "object") {
|
|
1036
|
+
+ const result = _findInObject(value, condition);
|
|
1037
|
+
if (result !== undefined) {
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
+
|
|
1043
|
+
+ return undefined;
|
|
1044
|
+
}
|