@cleandns/whois-rdap 1.0.51 → 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 -80
- 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 -82
- 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,360 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
// Import polyfills first
|
|
4
|
+
import "./polyfills.js";
|
|
5
|
+
import { validateDomain } from "./utils/validateDomain.js";
|
|
6
|
+
import { escapeRegex } from "./utils/escapeRegex.js";
|
|
7
|
+
import { toArray } from "./utils/toArray.js";
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// toArray utility tests
|
|
10
|
+
// ============================================================================
|
|
11
|
+
describe("toArray utility function", () => {
|
|
12
|
+
it("returns empty array for null", () => {
|
|
13
|
+
assert.deepStrictEqual(toArray(null), []);
|
|
14
|
+
});
|
|
15
|
+
it("returns empty array for undefined", () => {
|
|
16
|
+
assert.deepStrictEqual(toArray(undefined), []);
|
|
17
|
+
});
|
|
18
|
+
it("returns same array if already an array", () => {
|
|
19
|
+
const arr = [1, 2, 3];
|
|
20
|
+
assert.deepStrictEqual(toArray(arr), arr);
|
|
21
|
+
});
|
|
22
|
+
it("converts object to array of values", () => {
|
|
23
|
+
const obj = { a: 1, b: 2, c: 3 };
|
|
24
|
+
assert.deepStrictEqual(toArray(obj), [1, 2, 3]);
|
|
25
|
+
});
|
|
26
|
+
it("handles empty array", () => {
|
|
27
|
+
assert.deepStrictEqual(toArray([]), []);
|
|
28
|
+
});
|
|
29
|
+
it("handles empty object", () => {
|
|
30
|
+
assert.deepStrictEqual(toArray({}), []);
|
|
31
|
+
});
|
|
32
|
+
it("handles nested arrays", () => {
|
|
33
|
+
const arr = [[1, 2], [3, 4]];
|
|
34
|
+
assert.deepStrictEqual(toArray(arr), [[1, 2], [3, 4]]);
|
|
35
|
+
});
|
|
36
|
+
it("handles array-like objects with numeric keys", () => {
|
|
37
|
+
const obj = { "0": "a", "1": "b", "2": "c" };
|
|
38
|
+
assert.deepStrictEqual(toArray(obj), ["a", "b", "c"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("toArray handles RDAP edge cases (issue #16)", () => {
|
|
42
|
+
it("handles entities as object instead of array", () => {
|
|
43
|
+
const entities = {
|
|
44
|
+
"0": { roles: ["registrar"], handle: "123" },
|
|
45
|
+
"1": { roles: ["abuse"], handle: "456" },
|
|
46
|
+
};
|
|
47
|
+
const result = toArray(entities);
|
|
48
|
+
assert.strictEqual(Array.isArray(result), true);
|
|
49
|
+
assert.strictEqual(result.length, 2);
|
|
50
|
+
assert.deepStrictEqual(result[0], { roles: ["registrar"], handle: "123" });
|
|
51
|
+
});
|
|
52
|
+
it("handles nested entities being null", () => {
|
|
53
|
+
const result = toArray(null);
|
|
54
|
+
assert.deepStrictEqual(result, []);
|
|
55
|
+
});
|
|
56
|
+
it("handles nested entities being undefined", () => {
|
|
57
|
+
const result = toArray(undefined);
|
|
58
|
+
assert.deepStrictEqual(result, []);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// escapeRegex utility tests (ReDoS prevention)
|
|
63
|
+
// ============================================================================
|
|
64
|
+
describe("escapeRegex utility function", () => {
|
|
65
|
+
it("escapes special regex characters", () => {
|
|
66
|
+
const input = "test.*+?^${}()|[]\\";
|
|
67
|
+
const escaped = escapeRegex(input);
|
|
68
|
+
assert.strictEqual(escaped, "test\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\");
|
|
69
|
+
});
|
|
70
|
+
it("leaves normal strings unchanged", () => {
|
|
71
|
+
const input = "GoDaddy Inc";
|
|
72
|
+
const escaped = escapeRegex(input);
|
|
73
|
+
assert.strictEqual(escaped, "GoDaddy Inc");
|
|
74
|
+
});
|
|
75
|
+
it("handles empty string", () => {
|
|
76
|
+
const escaped = escapeRegex("");
|
|
77
|
+
assert.strictEqual(escaped, "");
|
|
78
|
+
});
|
|
79
|
+
it("handles string with only special characters", () => {
|
|
80
|
+
const input = ".*+";
|
|
81
|
+
const escaped = escapeRegex(input);
|
|
82
|
+
assert.strictEqual(escaped, "\\.\\*\\+");
|
|
83
|
+
});
|
|
84
|
+
it("escaped string can be used safely in RegExp", () => {
|
|
85
|
+
const malicious = "test(.*)+$";
|
|
86
|
+
const escaped = escapeRegex(malicious);
|
|
87
|
+
// Should not throw when creating RegExp
|
|
88
|
+
const regex = new RegExp(escaped, "i");
|
|
89
|
+
// The escaped string should match literally
|
|
90
|
+
assert.strictEqual(regex.test("test(.*)+$"), true);
|
|
91
|
+
assert.strictEqual(regex.test("testABC"), false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// validateDomain utility tests
|
|
96
|
+
// ============================================================================
|
|
97
|
+
describe("validateDomain utility function", () => {
|
|
98
|
+
it("accepts valid domain", () => {
|
|
99
|
+
const result = validateDomain("example.com");
|
|
100
|
+
assert.strictEqual(result, "example.com");
|
|
101
|
+
});
|
|
102
|
+
it("trims whitespace", () => {
|
|
103
|
+
const result = validateDomain(" example.com ");
|
|
104
|
+
assert.strictEqual(result, "example.com");
|
|
105
|
+
});
|
|
106
|
+
it("converts to lowercase", () => {
|
|
107
|
+
const result = validateDomain("EXAMPLE.COM");
|
|
108
|
+
assert.strictEqual(result, "example.com");
|
|
109
|
+
});
|
|
110
|
+
it("throws on empty string", () => {
|
|
111
|
+
assert.throws(() => validateDomain(""), /non-empty string/);
|
|
112
|
+
});
|
|
113
|
+
it("throws on whitespace-only string", () => {
|
|
114
|
+
assert.throws(() => validateDomain(" "), /non-empty string/);
|
|
115
|
+
});
|
|
116
|
+
it("throws on null", () => {
|
|
117
|
+
assert.throws(() => validateDomain(null), /non-empty string/);
|
|
118
|
+
});
|
|
119
|
+
it("throws on undefined", () => {
|
|
120
|
+
assert.throws(() => validateDomain(undefined), /non-empty string/);
|
|
121
|
+
});
|
|
122
|
+
it("throws on domain too long", () => {
|
|
123
|
+
const longDomain = "a".repeat(254) + ".com";
|
|
124
|
+
assert.throws(() => validateDomain(longDomain), /too long/);
|
|
125
|
+
});
|
|
126
|
+
it("accepts domain at max length", () => {
|
|
127
|
+
const maxDomain = "a".repeat(249) + ".com"; // 253 chars
|
|
128
|
+
const result = validateDomain(maxDomain);
|
|
129
|
+
assert.strictEqual(result.length, 253);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// vcardArray safety checks (issue #12)
|
|
134
|
+
// ============================================================================
|
|
135
|
+
describe("vcardArray safety checks (issue #12)", () => {
|
|
136
|
+
it("Array.isArray correctly identifies arrays", () => {
|
|
137
|
+
const vcardArray = ["vcard", [["fn", {}, "text", "Test"]]];
|
|
138
|
+
assert.strictEqual(Array.isArray(vcardArray[1]), true);
|
|
139
|
+
assert.strictEqual(typeof vcardArray[1].find, "function");
|
|
140
|
+
});
|
|
141
|
+
it("Array.isArray returns false for non-arrays", () => {
|
|
142
|
+
const vcardArray = ["vcard", "not-an-array"];
|
|
143
|
+
assert.strictEqual(Array.isArray(vcardArray[1]), false);
|
|
144
|
+
});
|
|
145
|
+
it("Array.isArray returns false for undefined", () => {
|
|
146
|
+
const vcardArray = ["vcard"];
|
|
147
|
+
assert.strictEqual(Array.isArray(vcardArray[1]), false);
|
|
148
|
+
});
|
|
149
|
+
it("safe access pattern prevents TypeError", () => {
|
|
150
|
+
const vcardArray = ["vcard"];
|
|
151
|
+
const hasFn = vcardArray && Array.isArray(vcardArray[1]) && vcardArray[1].find((el) => el[0] === 'fn');
|
|
152
|
+
assert.strictEqual(hasFn, false);
|
|
153
|
+
});
|
|
154
|
+
it("safe access pattern works with valid data", () => {
|
|
155
|
+
const vcardArray = ["vcard", [["fn", {}, "text", "Test Registrar"]]];
|
|
156
|
+
const hasFn = vcardArray && Array.isArray(vcardArray[1]) && vcardArray[1].find((el) => el[0] === 'fn');
|
|
157
|
+
assert.deepStrictEqual(hasFn, ["fn", {}, "text", "Test Registrar"]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// debug module integration (issue #13)
|
|
162
|
+
// ============================================================================
|
|
163
|
+
describe("debug module integration (issue #13)", () => {
|
|
164
|
+
it("debug module is importable", async () => {
|
|
165
|
+
const createDebug = (await import("debug")).default;
|
|
166
|
+
assert.strictEqual(typeof createDebug, "function");
|
|
167
|
+
});
|
|
168
|
+
it("debug instance can be created", async () => {
|
|
169
|
+
const createDebug = (await import("debug")).default;
|
|
170
|
+
const debug = createDebug("test:namespace");
|
|
171
|
+
assert.strictEqual(typeof debug, "function");
|
|
172
|
+
});
|
|
173
|
+
it("debug function can be called without error", async () => {
|
|
174
|
+
const createDebug = (await import("debug")).default;
|
|
175
|
+
const debug = createDebug("test:namespace");
|
|
176
|
+
// Should not throw
|
|
177
|
+
debug("test message %s", "arg");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// findTimestamps anti-pattern fix (commit comment)
|
|
182
|
+
// ============================================================================
|
|
183
|
+
describe("findTimestamps behavior", () => {
|
|
184
|
+
it("properly extracts timestamps from events array", () => {
|
|
185
|
+
var _a, _b, _c;
|
|
186
|
+
// This tests the fix for the anti-pattern where events.find was used with side effects
|
|
187
|
+
const events = [
|
|
188
|
+
{ eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
|
|
189
|
+
{ eventAction: "last changed", eventDate: "2023-06-15T12:00:00Z" },
|
|
190
|
+
{ eventAction: "expiration", eventDate: "2025-01-01T00:00:00Z" },
|
|
191
|
+
];
|
|
192
|
+
// The function should properly iterate and extract all timestamps
|
|
193
|
+
// without relying on side effects in find()
|
|
194
|
+
const created = (_a = events.find(ev => ev.eventAction === "registration")) === null || _a === void 0 ? void 0 : _a.eventDate;
|
|
195
|
+
const updated = (_b = events.find(ev => ev.eventAction === "last changed")) === null || _b === void 0 ? void 0 : _b.eventDate;
|
|
196
|
+
const expires = (_c = events.find(ev => ev.eventAction === "expiration")) === null || _c === void 0 ? void 0 : _c.eventDate;
|
|
197
|
+
assert.strictEqual(created, "2020-01-01T00:00:00Z");
|
|
198
|
+
assert.strictEqual(updated, "2023-06-15T12:00:00Z");
|
|
199
|
+
assert.strictEqual(expires, "2025-01-01T00:00:00Z");
|
|
200
|
+
});
|
|
201
|
+
it("handles events with invalid dates", () => {
|
|
202
|
+
const events = [
|
|
203
|
+
{ eventAction: "registration", eventDate: "invalid-date" },
|
|
204
|
+
{ eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
|
|
205
|
+
];
|
|
206
|
+
// Should skip invalid dates and find valid one
|
|
207
|
+
let validDate = null;
|
|
208
|
+
for (const ev of events) {
|
|
209
|
+
if (ev.eventAction === "registration" && ev.eventDate) {
|
|
210
|
+
const d = new Date(ev.eventDate);
|
|
211
|
+
if (!isNaN(d.valueOf())) {
|
|
212
|
+
validDate = d;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
assert.notStrictEqual(validDate, null);
|
|
218
|
+
assert.strictEqual(validDate === null || validDate === void 0 ? void 0 : validDate.toISOString(), "2020-01-01T00:00:00.000Z");
|
|
219
|
+
});
|
|
220
|
+
it("handles +0000Z date format", () => {
|
|
221
|
+
const dateStr = "2020-01-01T00:00:00+0000Z";
|
|
222
|
+
const normalized = dateStr.replace(/\+0000Z$/, "Z");
|
|
223
|
+
const d = new Date(normalized);
|
|
224
|
+
assert.strictEqual(isNaN(d.valueOf()), false);
|
|
225
|
+
assert.strictEqual(d.toISOString(), "2020-01-01T00:00:00.000Z");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// findInObject null safety
|
|
230
|
+
// ============================================================================
|
|
231
|
+
describe("findInObject null safety", () => {
|
|
232
|
+
it("handles null input", async () => {
|
|
233
|
+
const { findInObject } = await import("./utils/findInObject.js");
|
|
234
|
+
const result = findInObject(null, () => true, (el) => el, "fallback");
|
|
235
|
+
assert.strictEqual(result, "fallback");
|
|
236
|
+
});
|
|
237
|
+
it("handles undefined input", async () => {
|
|
238
|
+
const { findInObject } = await import("./utils/findInObject.js");
|
|
239
|
+
const result = findInObject(undefined, () => true, (el) => el, "fallback");
|
|
240
|
+
assert.strictEqual(result, "fallback");
|
|
241
|
+
});
|
|
242
|
+
it("handles object with null values", async () => {
|
|
243
|
+
const { findInObject } = await import("./utils/findInObject.js");
|
|
244
|
+
const obj = { a: null, b: { c: "found" } };
|
|
245
|
+
const result = findInObject(obj, (el) => el === "found", (el) => el, "fallback");
|
|
246
|
+
assert.strictEqual(result, "found");
|
|
247
|
+
});
|
|
248
|
+
it("finds nested value", async () => {
|
|
249
|
+
const { findInObject } = await import("./utils/findInObject.js");
|
|
250
|
+
const obj = { a: { b: { c: ["fn", {}, "text", "Test"] } } };
|
|
251
|
+
const result = findInObject(obj, (el) => Array.isArray(el) && el[0] === "fn", (el) => el[3], "fallback");
|
|
252
|
+
assert.strictEqual(result, "Test");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
// ============================================================================
|
|
256
|
+
// IP response parsing null safety
|
|
257
|
+
// ============================================================================
|
|
258
|
+
describe("IP response parsing", () => {
|
|
259
|
+
it("handles missing port43 field", async () => {
|
|
260
|
+
const { parseIpResponse } = await import("./ip.js");
|
|
261
|
+
const response = {
|
|
262
|
+
found: false,
|
|
263
|
+
registrar: { id: 0, name: null },
|
|
264
|
+
};
|
|
265
|
+
const rdap = {
|
|
266
|
+
handle: "NET-1-0-0-0-1",
|
|
267
|
+
startAddress: "1.0.0.0",
|
|
268
|
+
endAddress: "1.255.255.255",
|
|
269
|
+
// port43 is missing
|
|
270
|
+
};
|
|
271
|
+
// Should not throw
|
|
272
|
+
parseIpResponse("1.0.0.1", rdap, response);
|
|
273
|
+
assert.strictEqual(response.registrar.name, "");
|
|
274
|
+
});
|
|
275
|
+
it("handles port43 without expected pattern", async () => {
|
|
276
|
+
const { parseIpResponse } = await import("./ip.js");
|
|
277
|
+
const response = {
|
|
278
|
+
found: false,
|
|
279
|
+
registrar: { id: 0, name: null },
|
|
280
|
+
};
|
|
281
|
+
const rdap = {
|
|
282
|
+
handle: "NET-1-0-0-0-1",
|
|
283
|
+
port43: "whois-server", // No dots
|
|
284
|
+
};
|
|
285
|
+
// Should not throw
|
|
286
|
+
parseIpResponse("1.0.0.1", rdap, response);
|
|
287
|
+
assert.strictEqual(response.registrar.name, "");
|
|
288
|
+
});
|
|
289
|
+
it("extracts registry from valid port43", async () => {
|
|
290
|
+
const { parseIpResponse } = await import("./ip.js");
|
|
291
|
+
const response = {
|
|
292
|
+
found: false,
|
|
293
|
+
registrar: { id: 0, name: null },
|
|
294
|
+
};
|
|
295
|
+
const rdap = {
|
|
296
|
+
handle: "NET-1-0-0-0-1",
|
|
297
|
+
port43: "whois.arin.net",
|
|
298
|
+
};
|
|
299
|
+
parseIpResponse("1.0.0.1", rdap, response);
|
|
300
|
+
assert.strictEqual(response.registrar.name, "ARIN");
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// String.prototype.toWellFormed polyfill tests (Node.js 18 compatibility)
|
|
305
|
+
// ============================================================================
|
|
306
|
+
describe("String.prototype.toWellFormed polyfill", () => {
|
|
307
|
+
it("toWellFormed is available as a function", () => {
|
|
308
|
+
assert.strictEqual(typeof "".toWellFormed, "function");
|
|
309
|
+
});
|
|
310
|
+
it("returns same string for well-formed input", () => {
|
|
311
|
+
const str = "Hello, World!";
|
|
312
|
+
assert.strictEqual(str.toWellFormed(), str);
|
|
313
|
+
});
|
|
314
|
+
it("handles empty string", () => {
|
|
315
|
+
assert.strictEqual("".toWellFormed(), "");
|
|
316
|
+
});
|
|
317
|
+
it("handles valid surrogate pairs (emoji)", () => {
|
|
318
|
+
const emoji = "😀"; // U+1F600 = \uD83D\uDE00
|
|
319
|
+
assert.strictEqual(emoji.toWellFormed(), emoji);
|
|
320
|
+
});
|
|
321
|
+
it("replaces lone high surrogate with U+FFFD", () => {
|
|
322
|
+
const loneHigh = "abc\uD800def";
|
|
323
|
+
assert.strictEqual(loneHigh.toWellFormed(), "abc\uFFFDdef");
|
|
324
|
+
});
|
|
325
|
+
it("replaces lone low surrogate with U+FFFD", () => {
|
|
326
|
+
const loneLow = "abc\uDC00def";
|
|
327
|
+
assert.strictEqual(loneLow.toWellFormed(), "abc\uFFFDdef");
|
|
328
|
+
});
|
|
329
|
+
it("replaces lone high surrogate at end", () => {
|
|
330
|
+
const str = "test\uD800";
|
|
331
|
+
assert.strictEqual(str.toWellFormed(), "test\uFFFD");
|
|
332
|
+
});
|
|
333
|
+
it("handles multiple lone surrogates", () => {
|
|
334
|
+
const str = "\uD800\uD800";
|
|
335
|
+
assert.strictEqual(str.toWellFormed(), "\uFFFD\uFFFD");
|
|
336
|
+
});
|
|
337
|
+
it("preserves valid surrogate pairs among lone surrogates", () => {
|
|
338
|
+
const str = "\uD800\uD83D\uDE00\uDC00"; // lone high, valid pair, lone low
|
|
339
|
+
assert.strictEqual(str.toWellFormed(), "\uFFFD😀\uFFFD");
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe("String.prototype.isWellFormed polyfill", () => {
|
|
343
|
+
it("isWellFormed is available as a function", () => {
|
|
344
|
+
assert.strictEqual(typeof "".isWellFormed, "function");
|
|
345
|
+
});
|
|
346
|
+
it("returns true for well-formed strings", () => {
|
|
347
|
+
assert.strictEqual("Hello".isWellFormed(), true);
|
|
348
|
+
assert.strictEqual("".isWellFormed(), true);
|
|
349
|
+
assert.strictEqual("😀".isWellFormed(), true);
|
|
350
|
+
});
|
|
351
|
+
it("returns false for lone high surrogate", () => {
|
|
352
|
+
assert.strictEqual("test\uD800".isWellFormed(), false);
|
|
353
|
+
});
|
|
354
|
+
it("returns false for lone low surrogate", () => {
|
|
355
|
+
assert.strictEqual("test\uDC00".isWellFormed(), false);
|
|
356
|
+
});
|
|
357
|
+
it("returns true for valid surrogate pair", () => {
|
|
358
|
+
assert.strictEqual("\uD83D\uDE00".isWellFormed(), true);
|
|
359
|
+
});
|
|
360
|
+
});
|
package/dist/ip.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export function parseIpResponse(ip, rdap, response) {
|
|
2
2
|
var _a, _b, _c;
|
|
3
3
|
response.found = Boolean(rdap.handle);
|
|
4
|
-
|
|
4
|
+
// Safely extract registry from port43 with null check
|
|
5
|
+
let registry = '';
|
|
6
|
+
if (rdap.port43) {
|
|
7
|
+
const match = rdap.port43.match(/\.(\w+)\./);
|
|
8
|
+
if (match) {
|
|
9
|
+
registry = match[1].toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
5
12
|
const realRdapServer = (_c = (_b = (_a = rdap.links) === null || _a === void 0 ? void 0 : _a.find(({ rel }) => rel === 'self')) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.replace(/\/ip\/.*/, '/ip/');
|
|
6
13
|
response.server = realRdapServer || 'https://rdap.org/ip/';
|
|
7
14
|
response.identity = {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyfills for Node.js 18 compatibility.
|
|
3
|
+
* String.prototype.toWellFormed was added in Node.js 20.
|
|
4
|
+
*/
|
|
5
|
+
// Polyfill for String.prototype.toWellFormed (ES2024)
|
|
6
|
+
// Replaces lone surrogates with U+FFFD (replacement character)
|
|
7
|
+
if (typeof String.prototype.toWellFormed !== 'function') {
|
|
8
|
+
String.prototype.toWellFormed = function () {
|
|
9
|
+
const str = String(this);
|
|
10
|
+
const len = str.length;
|
|
11
|
+
let result = '';
|
|
12
|
+
for (let i = 0; i < len; i++) {
|
|
13
|
+
const code = str.charCodeAt(i);
|
|
14
|
+
// Check for lone surrogates
|
|
15
|
+
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
16
|
+
// High surrogate
|
|
17
|
+
if (i + 1 < len) {
|
|
18
|
+
const next = str.charCodeAt(i + 1);
|
|
19
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
20
|
+
// Valid surrogate pair
|
|
21
|
+
result += str[i] + str[i + 1];
|
|
22
|
+
i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Lone high surrogate - replace with U+FFFD
|
|
27
|
+
result += '\uFFFD';
|
|
28
|
+
}
|
|
29
|
+
else if (code >= 0xDC00 && code <= 0xDFFF) {
|
|
30
|
+
// Lone low surrogate - replace with U+FFFD
|
|
31
|
+
result += '\uFFFD';
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
result += str[i];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Polyfill for String.prototype.isWellFormed (ES2024)
|
|
41
|
+
if (typeof String.prototype.isWellFormed !== 'function') {
|
|
42
|
+
String.prototype.isWellFormed = function () {
|
|
43
|
+
const str = String(this);
|
|
44
|
+
const len = str.length;
|
|
45
|
+
for (let i = 0; i < len; i++) {
|
|
46
|
+
const code = str.charCodeAt(i);
|
|
47
|
+
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
48
|
+
// High surrogate - check for valid pair
|
|
49
|
+
if (i + 1 >= len)
|
|
50
|
+
return false;
|
|
51
|
+
const next = str.charCodeAt(i + 1);
|
|
52
|
+
if (next < 0xDC00 || next > 0xDFFF)
|
|
53
|
+
return false;
|
|
54
|
+
i++; // Skip the low surrogate
|
|
55
|
+
}
|
|
56
|
+
else if (code >= 0xDC00 && code <= 0xDFFF) {
|
|
57
|
+
// Lone low surrogate
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {};
|
package/dist/port43.js
CHANGED
|
@@ -4,6 +4,10 @@ import { Socket } from "net";
|
|
|
4
4
|
import { port43servers, port43parsers } from "./port43servers.js";
|
|
5
5
|
import { ianaToRegistrarCache } from "./utils/ianaIdToRegistrar.js";
|
|
6
6
|
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
7
|
+
import createDebug from "debug";
|
|
8
|
+
import { escapeRegex } from "./utils/escapeRegex.js";
|
|
9
|
+
// Debug logger - enable with DEBUG=whois:* environment variable
|
|
10
|
+
const debug = createDebug("whois:port43");
|
|
7
11
|
export function determinePort43Domain(actor) {
|
|
8
12
|
const parsed = parseDomain(actor);
|
|
9
13
|
if (parsed.type === ParseResultType.Listed) {
|
|
@@ -30,7 +34,7 @@ export async function port43(actor, _fetch) {
|
|
|
30
34
|
? opts.query.replace("$addr", domain)
|
|
31
35
|
: `${domain}\r\n`;
|
|
32
36
|
const port = (opts === null || opts === void 0 ? void 0 : opts.port) || 43;
|
|
33
|
-
|
|
37
|
+
debug("looking up %s on %s:%d", domain, server, port);
|
|
34
38
|
const response = {
|
|
35
39
|
found: true,
|
|
36
40
|
statusCode: 200,
|
|
@@ -73,7 +77,7 @@ export async function port43(actor, _fetch) {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
catch (error) {
|
|
76
|
-
|
|
80
|
+
debug("port43 lookup error: %O", { port, server, query, error: error.message });
|
|
77
81
|
response.found = false;
|
|
78
82
|
response.statusCode = 500;
|
|
79
83
|
response.error = error.message || "Unknown error during port 43 lookup";
|
|
@@ -82,7 +86,6 @@ export async function port43(actor, _fetch) {
|
|
|
82
86
|
return response;
|
|
83
87
|
}
|
|
84
88
|
port43response = port43response.replace(/^[ \t]+/gm, "");
|
|
85
|
-
// console.log(port43response);
|
|
86
89
|
let m;
|
|
87
90
|
if (m = port43response.match(/^%*\s*(NOT FOUND|No match|NO OBJECT FOUND|No entries found|No Data Found|Domain is available for registration|No information available|Status: free)\b/im)) {
|
|
88
91
|
response.found = false;
|
|
@@ -112,10 +115,6 @@ export async function port43(actor, _fetch) {
|
|
|
112
115
|
!response.reseller &&
|
|
113
116
|
(m = port43response.match(/^(?:Reseller(?: Name)?|reseller_name|reseller):[ \t]*(\S.+)/im)) &&
|
|
114
117
|
(response.reseller = m[1].trim());
|
|
115
|
-
// console.log(port43response)
|
|
116
|
-
// Updated Date: 2024-11-21T13:42:54Z
|
|
117
|
-
// Creation Date: 2017-12-16T02:11:08Z
|
|
118
|
-
// Registry Expiry Date: 2031-07-10T02:11:08Z
|
|
119
118
|
!response.ts.updated &&
|
|
120
119
|
(m = port43response.match(/^(?:Last Modified|Updated Date|Last updated on|domain_datelastmodified|last-update|modified|last modified)\.*:[ \t]*(\S.+)/im)) &&
|
|
121
120
|
(response.ts.updated = new Date(reformatDate(m[1])) || null);
|
|
@@ -169,6 +168,7 @@ export async function port43(actor, _fetch) {
|
|
|
169
168
|
response.ts.updated = null;
|
|
170
169
|
if (response.ts.expires && !response.ts.expires.valueOf())
|
|
171
170
|
response.ts.expires = null;
|
|
171
|
+
// Match registrar name against IANA cache using escaped regex to prevent ReDoS
|
|
172
172
|
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
173
173
|
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
174
174
|
if (name === response.registrar.name) {
|
|
@@ -177,19 +177,33 @@ export async function port43(actor, _fetch) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
180
|
+
if (response.registrar.id === 0 && response.registrar.name && response.registrar.name !== "") {
|
|
181
|
+
const escapedName = escapeRegex(response.registrar.name);
|
|
181
182
|
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
try {
|
|
184
|
+
if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
|
|
185
|
+
response.registrar.id = id;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (_o) {
|
|
190
|
+
// Skip if regex still fails for some reason
|
|
191
|
+
continue;
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
if (response.registrar.id === 0 && response.registrar.name) {
|
|
196
|
+
const escapedName = escapeRegex(response.registrar.name.replace(/,.*/, ""));
|
|
189
197
|
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
try {
|
|
199
|
+
if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
|
|
200
|
+
response.registrar.id = id;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (_p) {
|
|
205
|
+
// Skip if regex still fails for some reason
|
|
206
|
+
continue;
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
}
|
|
@@ -3,15 +3,25 @@ export function findInObject(obj, condition, extractor, fallback) {
|
|
|
3
3
|
return found === undefined ? fallback : extractor(found);
|
|
4
4
|
}
|
|
5
5
|
function _findInObject(obj, condition) {
|
|
6
|
+
// Handle null/undefined
|
|
7
|
+
if (obj === null || obj === undefined) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
6
10
|
for (const key in obj) {
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
// Skip inherited properties
|
|
12
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const value = obj[key];
|
|
16
|
+
if (condition(value)) {
|
|
17
|
+
return value;
|
|
9
18
|
}
|
|
10
|
-
if (typeof
|
|
11
|
-
const result = _findInObject(
|
|
19
|
+
if (value !== null && typeof value === "object") {
|
|
20
|
+
const result = _findInObject(value, condition);
|
|
12
21
|
if (result !== undefined) {
|
|
13
22
|
return result;
|
|
14
23
|
}
|
|
15
24
|
}
|
|
16
25
|
}
|
|
26
|
+
return undefined;
|
|
17
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findNameservers(values: any[]): string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function findNameservers(values) {
|
|
2
|
+
let nameservers = [];
|
|
3
|
+
if (Array.isArray(values)) {
|
|
4
|
+
nameservers = values;
|
|
5
|
+
}
|
|
6
|
+
else if (typeof values === "object") {
|
|
7
|
+
nameservers = Object.values(values);
|
|
8
|
+
}
|
|
9
|
+
return nameservers
|
|
10
|
+
.map((ns) => { var _a; return ns.ldhName || ns.ldnName || ((_a = ns.ipAddresses) === null || _a === void 0 ? void 0 : _a.v4); })
|
|
11
|
+
.flat()
|
|
12
|
+
.filter((ns) => ns)
|
|
13
|
+
.map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findStatus(statuses: string | string[], domain: string): string[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { normalizeWhoisStatus } from "../whoisStatus.js";
|
|
2
|
+
export function findStatus(statuses, domain) {
|
|
3
|
+
return (Array.isArray(statuses)
|
|
4
|
+
? statuses
|
|
5
|
+
: statuses && typeof statuses === "object"
|
|
6
|
+
? Object.keys(statuses)
|
|
7
|
+
: typeof statuses === "string"
|
|
8
|
+
? statuses.trim().split(/\s*,\s*/)
|
|
9
|
+
: []).map((status) => normalizeWhoisStatus(status));
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WhoisTimestampFields } from "../../whois.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts timestamps from RDAP events array.
|
|
4
|
+
* Properly iterates through events and breaks when a match is found.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findTimestamps(values: any[]): Record<WhoisTimestampFields, Date | null>;
|