@cleandns/whois-rdap 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +178 -0
- package/dist/port43.d.ts +3 -0
- package/dist/port43.js +121 -0
- package/dist/port43servers.d.ts +3 -0
- package/dist/port43servers.js +800 -0
- package/dist/utils/findInObject.d.ts +1 -0
- package/dist/utils/findInObject.js +17 -0
- package/dist/utils/fixArrays.d.ts +1 -0
- package/dist/utils/fixArrays.js +15 -0
- package/dist/utils/ianaIdToRegistrar.d.ts +11 -0
- package/dist/utils/ianaIdToRegistrar.js +16 -0
- package/dist/utils/tldToRdap.d.ts +1 -0
- package/dist/utils/tldToRdap.js +61 -0
- package/dist/whoisStatus.d.ts +1 -0
- package/dist/whoisStatus.js +36 -0
- package/package.json +27 -0
- package/src/index.ts +246 -0
- package/src/port43.ts +164 -0
- package/src/port43servers.ts +827 -0
- package/src/utils/findInObject.ts +23 -0
- package/src/utils/fixArrays.ts +17 -0
- package/src/utils/ianaIdToRegistrar.ts +34 -0
- package/src/utils/tldToRdap.ts +73 -0
- package/src/whoisStatus.ts +43 -0
- package/tsconfig.json +109 -0
- package/whois.d.ts +17 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findInObject(obj: Object, condition: (el: any) => boolean, extractor: (el: any) => any, fallback: any): any;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function findInObject(obj, condition, extractor, fallback) {
|
|
2
|
+
const found = _findInObject(obj, condition);
|
|
3
|
+
return found === undefined ? fallback : extractor(found);
|
|
4
|
+
}
|
|
5
|
+
function _findInObject(obj, condition) {
|
|
6
|
+
for (const key in obj) {
|
|
7
|
+
if (condition(obj[key])) {
|
|
8
|
+
return obj[key];
|
|
9
|
+
}
|
|
10
|
+
if (typeof obj[key] === "object") {
|
|
11
|
+
const result = _findInObject(obj[key], condition);
|
|
12
|
+
if (result !== undefined) {
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fixArrays(data: any): any;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// turn an object with sequential numeric keys into an array
|
|
2
|
+
export function fixArrays(data) {
|
|
3
|
+
if (Array.isArray(data)) {
|
|
4
|
+
data = data.map(fixArrays);
|
|
5
|
+
}
|
|
6
|
+
else if (data && typeof data === "object") {
|
|
7
|
+
if (data['0']) {
|
|
8
|
+
data = Object.values(data).map(fixArrays);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
data = Object.fromEntries(Object.entries(data).map(([key, value]) => [key, fixArrays(value)]));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type IanaRegistrarId = number;
|
|
2
|
+
type IanaRegistrarStatus = "Terminated" | "Accredited" | "Reserved";
|
|
3
|
+
interface IanaRegistrar {
|
|
4
|
+
id: IanaRegistrarId;
|
|
5
|
+
name: string;
|
|
6
|
+
status: IanaRegistrarStatus;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const ianaToRegistrarCache: Map<number, IanaRegistrar>;
|
|
10
|
+
export declare function ianaIdToRegistrar(item: number): Promise<IanaRegistrar | undefined>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const ianaToRegistrarCache = new Map();
|
|
2
|
+
export async function ianaIdToRegistrar(item) {
|
|
3
|
+
if (ianaToRegistrarCache.size === 0) {
|
|
4
|
+
// console.warn(`fetching iana-to-registrar`);
|
|
5
|
+
const response = await fetch(`https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv`).then((r) => r.text());
|
|
6
|
+
const records = response.matchAll(/^(?:"(\d+)"|(\d+)),(?:"([^\n\r"]*)"|([^\n\r",]*)),(?:"([^\n\r"]*)"|([^\n\r",]*)),(?:"([^\n\r"]*)"|([^\n\r",]*))/gm);
|
|
7
|
+
for (const record of records) {
|
|
8
|
+
const id = parseInt(record[1] || record[2]);
|
|
9
|
+
const name = record[3] || record[4];
|
|
10
|
+
const status = (record[5] || record[6]);
|
|
11
|
+
const url = record[7] || record[8];
|
|
12
|
+
ianaToRegistrarCache.set(id, { id, name, status, url });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return ianaToRegistrarCache.get(item);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function tldToRdap(domain: string): Promise<[string, string | null]>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ParseResultType, parseDomain } from "parse-domain";
|
|
2
|
+
const tldCache = new Map([]);
|
|
3
|
+
const tldCachePresets = [
|
|
4
|
+
["br.com", "https://rdap.centralnic.com/br.com"],
|
|
5
|
+
["cn.com", "https://rdap.centralnic.com/cn.com"],
|
|
6
|
+
["de.com", "https://rdap.centralnic.com/de.com"],
|
|
7
|
+
["eu.com", "https://rdap.centralnic.com/eu.com"],
|
|
8
|
+
["gb.com", "https://rdap.centralnic.com/gb.com"],
|
|
9
|
+
["gb.net", "https://rdap.centralnic.com/gb.net"],
|
|
10
|
+
["gr.com", "https://rdap.centralnic.com/gr.com"],
|
|
11
|
+
["hu.com", "https://rdap.centralnic.com/hu.com"],
|
|
12
|
+
["in.net", "https://rdap.centralnic.com/in.net"],
|
|
13
|
+
["jpn.com", "https://rdap.centralnic.com/jpn.com"],
|
|
14
|
+
["no.com", "https://rdap.centralnic.com/no.com"],
|
|
15
|
+
["qc.com", "https://rdap.centralnic.com/qc.com"],
|
|
16
|
+
["ru.com", "https://rdap.centralnic.com/ru.com"],
|
|
17
|
+
["sa.com", "https://rdap.centralnic.com/sa.com"],
|
|
18
|
+
["se.com", "https://rdap.centralnic.com/se.com"],
|
|
19
|
+
["se.net", "https://rdap.centralnic.com/se.net"],
|
|
20
|
+
["uk.com", "https://rdap.centralnic.com/uk.com"],
|
|
21
|
+
["uk.net", "https://rdap.centralnic.com/uk.net"],
|
|
22
|
+
["us.com", "https://rdap.centralnic.com/us.com"],
|
|
23
|
+
["uy.com", "https://rdap.centralnic.com/uy.com"],
|
|
24
|
+
["web.com", "https://rdap.centralnic.com/web.com"],
|
|
25
|
+
["za.com", "https://rdap.centralnic.com/za.com"],
|
|
26
|
+
];
|
|
27
|
+
export async function tldToRdap(domain) {
|
|
28
|
+
if (tldCache.size === 0) {
|
|
29
|
+
for (const [tld, url] of tldCachePresets) {
|
|
30
|
+
tldCache.set(tld, url);
|
|
31
|
+
}
|
|
32
|
+
// console.warn(`fetching tld-to-rdap`);
|
|
33
|
+
const response = await fetch(`https://data.iana.org/rdap/dns.json`).then((r) => r.json());
|
|
34
|
+
for (const [tlds, urls] of response.services) {
|
|
35
|
+
for (const tld of tlds) {
|
|
36
|
+
tldCache.set(tld, urls[0].replace(/\/$/, ''));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parsed = parseDomain(domain);
|
|
41
|
+
if (parsed.type === ParseResultType.Listed) {
|
|
42
|
+
let tld = parsed.topLevelDomains.join(".");
|
|
43
|
+
if (tldCache.has(tld)) {
|
|
44
|
+
return [parsed.domain + '.' + tld, tldCache.get(tld)];
|
|
45
|
+
}
|
|
46
|
+
tld = parsed.icann.topLevelDomains.join(".");
|
|
47
|
+
if (tldCache.has(tld)) {
|
|
48
|
+
return [parsed.icann.domain + '.' + tld, tldCache.get(tld)];
|
|
49
|
+
}
|
|
50
|
+
// const tlds = [
|
|
51
|
+
// parsed.topLevelDomains.join("."),
|
|
52
|
+
// // parsed.icann.topLevelDomains.join("."),
|
|
53
|
+
// ];
|
|
54
|
+
// for (const tld of tlds) {
|
|
55
|
+
// if (tldCache.has(tld)) {
|
|
56
|
+
// return tldCache.get(tld);
|
|
57
|
+
// }
|
|
58
|
+
// }
|
|
59
|
+
}
|
|
60
|
+
return [domain, null];
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeWhoisStatus(status: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const statusMap = {
|
|
2
|
+
'inactive': 'inactive',
|
|
3
|
+
'active': 'active',
|
|
4
|
+
'ok': 'active',
|
|
5
|
+
'pendingcreate': 'pending create',
|
|
6
|
+
'pendingdelete': 'pending delete',
|
|
7
|
+
'pendingrenew': 'pending renew',
|
|
8
|
+
'pendingrestore': 'pending restore',
|
|
9
|
+
'pendingtransfer': 'pending transfer',
|
|
10
|
+
'pendingupdate': 'pending update',
|
|
11
|
+
'addperiod': 'add period',
|
|
12
|
+
'autorenewperiod': 'auto renew period',
|
|
13
|
+
'redemptionperiod': 'redemption period',
|
|
14
|
+
'renewperiod': 'renew period',
|
|
15
|
+
'transferperiod': 'transfer period',
|
|
16
|
+
'serverhold': 'server hold',
|
|
17
|
+
'serverdeleteprohibited': 'server delete prohibited',
|
|
18
|
+
'serverdeletedprohibited': 'server delete prohibited',
|
|
19
|
+
'serverrenewprohibited': 'server renew prohibited',
|
|
20
|
+
'servertransferprohibited': 'server transfer prohibited',
|
|
21
|
+
'serverupdateprohibited': 'server update prohibited',
|
|
22
|
+
'clienthold': 'client hold',
|
|
23
|
+
'clientdeleteprohibited': 'client delete prohibited',
|
|
24
|
+
'clientrenewprohibited': 'client renew prohibited',
|
|
25
|
+
'clienttransferprohibited': 'client transfer prohibited',
|
|
26
|
+
'clientupdateprohibited': 'client update prohibited',
|
|
27
|
+
'hold': 'client hold',
|
|
28
|
+
'deleteprohibited': 'client delete prohibited',
|
|
29
|
+
'renewprohibited': 'client renew prohibited',
|
|
30
|
+
'transferprohibited': 'client transfer prohibited',
|
|
31
|
+
'updatedprohibited': 'client update prohibited',
|
|
32
|
+
};
|
|
33
|
+
export function normalizeWhoisStatus(status) {
|
|
34
|
+
status = (status || "").toLocaleLowerCase();
|
|
35
|
+
return statusMap[status.replace(/[ _]/g, '')] || status;
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cleandns/whois-rdap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/cleandns-inc/tool-whois.git"
|
|
13
|
+
},
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/cleandns-inc/tool-whois/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/cleandns-inc/tool-whois#readme",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.4.3"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"parse-domain": "^8.0.2",
|
|
25
|
+
"promise-socket": "^7.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { WhoisOptions, WhoisResponse, WhoisTimestampFields } from "../whois.js";
|
|
2
|
+
import { determinePort43Domain, port43 } from "./port43.js";
|
|
3
|
+
import { findInObject } from "./utils/findInObject.js";
|
|
4
|
+
import { fixArrays } from "./utils/fixArrays.js";
|
|
5
|
+
import { ianaIdToRegistrar } from "./utils/ianaIdToRegistrar.js";
|
|
6
|
+
import { tldToRdap } from "./utils/tldToRdap.js";
|
|
7
|
+
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
8
|
+
|
|
9
|
+
const eventMap = new Map<string, WhoisTimestampFields>([
|
|
10
|
+
["registration", "created"],
|
|
11
|
+
["last changed", "updated"],
|
|
12
|
+
["expiration", "expires"],
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export async function whois(domain: string, options: WhoisOptions = { fetch: fetch }): Promise<WhoisResponse> {
|
|
16
|
+
const fetch = options.fetch!;
|
|
17
|
+
|
|
18
|
+
let url: string | null = null;
|
|
19
|
+
[domain, url] = await tldToRdap(domain);
|
|
20
|
+
|
|
21
|
+
const response: WhoisResponse = {
|
|
22
|
+
found: false,
|
|
23
|
+
registrar: { id: 0, name: null },
|
|
24
|
+
reseller: null,
|
|
25
|
+
status: [],
|
|
26
|
+
nameservers: [],
|
|
27
|
+
ts: { created: null, updated: null, expires: null },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (url === null) {
|
|
31
|
+
if (determinePort43Domain(domain)[2]) {
|
|
32
|
+
return port43(domain);
|
|
33
|
+
}
|
|
34
|
+
url = "https://rdap.org";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const thinRdap = `${url}/domain/${domain}`;
|
|
38
|
+
// console.log(`fetching thin RDAP: ${thinRdap}`);
|
|
39
|
+
|
|
40
|
+
let thinResponse = await fetch(thinRdap)
|
|
41
|
+
.then((r) => r.json() as any)
|
|
42
|
+
.catch(() => null);
|
|
43
|
+
if (thinResponse && !thinResponse.errorCode) {
|
|
44
|
+
} else {
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (thinResponse?.rdapConformance?.["0"]) {
|
|
49
|
+
thinResponse = fixArrays(thinResponse);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const selfRdap = thinResponse.links?.find((link: any) => link.rel === "self");
|
|
53
|
+
const thickRdap = thinResponse.links
|
|
54
|
+
?.find(
|
|
55
|
+
(link: any) =>
|
|
56
|
+
link.href !== selfRdap?.href &&
|
|
57
|
+
link.rel === "related" &&
|
|
58
|
+
link.type === "application/rdap+json"
|
|
59
|
+
)
|
|
60
|
+
?.href.replace("/domain/domain/", "/domain/");
|
|
61
|
+
|
|
62
|
+
let thickResponse: any = null;
|
|
63
|
+
|
|
64
|
+
if (thickRdap) {
|
|
65
|
+
// console.log(`fetching thick RDAP: ${thickRdap}`);
|
|
66
|
+
thickResponse = await fetch(thickRdap)
|
|
67
|
+
.then((r) => r.json() as any)
|
|
68
|
+
.catch(() => null);
|
|
69
|
+
if (thickResponse && !thickResponse.errorCode && !thickResponse.error) {
|
|
70
|
+
} else {
|
|
71
|
+
thickResponse = null;
|
|
72
|
+
// console.warn(`thick RDAP failed for ${domain}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (thickResponse?.rdapConformance?.["0"]) {
|
|
77
|
+
thickResponse = fixArrays(thickResponse);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const registrars: any[] = [];
|
|
81
|
+
const resellers: any[] = [];
|
|
82
|
+
|
|
83
|
+
async function extractRegistrarsAndResellers(response: any, url: string) {
|
|
84
|
+
for (const ent of [
|
|
85
|
+
...(response.entities || []),
|
|
86
|
+
response.entity ? { events: response.events, ...response.entity } : null,
|
|
87
|
+
].filter(Boolean)) {
|
|
88
|
+
if (ent.roles?.includes("registrar") || ent.role === "registrar") {
|
|
89
|
+
const pubIds: any[] = [];
|
|
90
|
+
if (ent.publicIds) {
|
|
91
|
+
pubIds.push(
|
|
92
|
+
...(Array.isArray(ent.publicIds)
|
|
93
|
+
? ent.publicIds
|
|
94
|
+
: [[ent.publicIds]])
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (ent.publicIDs) {
|
|
98
|
+
pubIds.push(
|
|
99
|
+
...(Array.isArray(ent.publicIDs)
|
|
100
|
+
? ent.publicIDs
|
|
101
|
+
: [[ent.publicIDs]])
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const reg =
|
|
105
|
+
pubIds.find((id: any) => id.type === "PANDI Registrar ID")
|
|
106
|
+
?.Identifier ||
|
|
107
|
+
pubIds.find((id: any) => id.type === "PANDI Registrar ID")
|
|
108
|
+
?.identifier ||
|
|
109
|
+
pubIds.find((id: any) => id.type === "IANA Registrar ID")
|
|
110
|
+
?.Identifier ||
|
|
111
|
+
pubIds.find((id: any) => id.type === "IANA Registrar ID")?.identifier;
|
|
112
|
+
|
|
113
|
+
if (reg) {
|
|
114
|
+
const id = reg;
|
|
115
|
+
const name =
|
|
116
|
+
parseInt(id) == id
|
|
117
|
+
? (await ianaIdToRegistrar(parseInt(id)))?.name
|
|
118
|
+
: findInObject(
|
|
119
|
+
ent.vcardArray,
|
|
120
|
+
(el: any) =>
|
|
121
|
+
Array.isArray(el) && (el[0] === "fn" || el[0] === "org"),
|
|
122
|
+
(el: any[]) => el[3],
|
|
123
|
+
reg
|
|
124
|
+
);
|
|
125
|
+
const events =
|
|
126
|
+
ent.events || response.events || ent.enents || response.enents;
|
|
127
|
+
registrars.push({ id, name, events });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
(ent.roles?.includes("reseller") || ent.role === "reseller") &&
|
|
133
|
+
ent.vcardArray
|
|
134
|
+
) {
|
|
135
|
+
// vcard objects can be unexpectedly and arbitrarily nested
|
|
136
|
+
const name = findInObject(
|
|
137
|
+
ent.vcardArray,
|
|
138
|
+
(el: any) => Array.isArray(el) && (el[0] === "fn" || el[0] === "org"),
|
|
139
|
+
(el: any[]) => el[3],
|
|
140
|
+
""
|
|
141
|
+
);
|
|
142
|
+
resellers.push({ name });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (thickResponse && !thickResponse.errorCode) {
|
|
148
|
+
await extractRegistrarsAndResellers(thickResponse, thickRdap);
|
|
149
|
+
}
|
|
150
|
+
if (thinResponse && !thinResponse.errorCode) {
|
|
151
|
+
await extractRegistrarsAndResellers(thinResponse, thinRdap);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
response.found = true;
|
|
155
|
+
|
|
156
|
+
// registrar
|
|
157
|
+
const { events, ...registrar } = registrars.sort((a: any, b: any) => {
|
|
158
|
+
// console.warn({ a, b });
|
|
159
|
+
const aDate =
|
|
160
|
+
a.events.find((ev: any) => ev.eventAction === "registration")
|
|
161
|
+
?.eventDate || 0;
|
|
162
|
+
const bDate =
|
|
163
|
+
b.events.find((ev: any) => ev.eventAction === "registration")
|
|
164
|
+
?.eventDate || 0;
|
|
165
|
+
return new Date(bDate).valueOf() - new Date(aDate).valueOf();
|
|
166
|
+
})[0] || { id: 0, name: "" };
|
|
167
|
+
response.registrar = registrar;
|
|
168
|
+
|
|
169
|
+
// reseller
|
|
170
|
+
const reseller = resellers[0]?.name || "";
|
|
171
|
+
response.reseller = reseller;
|
|
172
|
+
|
|
173
|
+
// status
|
|
174
|
+
response.status = findStatus(
|
|
175
|
+
thickResponse?.status || thinResponse?.status || [],
|
|
176
|
+
domain
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// nameservers
|
|
180
|
+
response.nameservers = findNameservers(
|
|
181
|
+
thickResponse?.nameservers || thinResponse?.nameservers || []
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// ts
|
|
185
|
+
response.ts = findTimestamps(
|
|
186
|
+
thickResponse?.events || thinResponse?.events || []
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return response;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function findStatus(statuses: string | string[], domain: string): string[] {
|
|
193
|
+
// console.warn({ domain, statuses });
|
|
194
|
+
|
|
195
|
+
return (Array.isArray(statuses)
|
|
196
|
+
? statuses
|
|
197
|
+
: statuses && typeof statuses === "object"
|
|
198
|
+
? Object.keys(statuses)
|
|
199
|
+
: (statuses || "").trim().split(/\s*,\s*/)
|
|
200
|
+
).map((status) => normalizeWhoisStatus(status));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function findNameservers(values: any[]): string[] {
|
|
204
|
+
let nameservers: any[] = [];
|
|
205
|
+
if (Array.isArray(values)) {
|
|
206
|
+
nameservers = values;
|
|
207
|
+
} else if (typeof values === "object") {
|
|
208
|
+
nameservers = Object.values(values);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return nameservers
|
|
212
|
+
.map((ns) => ns.ldhName || ns.ldnName || ns.ipAddresses?.v4)
|
|
213
|
+
.flat()
|
|
214
|
+
.filter((ns) => ns)
|
|
215
|
+
.map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
|
|
216
|
+
.sort();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function findTimestamps(values: any[]) {
|
|
220
|
+
const ts: Record<WhoisTimestampFields, Date | null> = {
|
|
221
|
+
created: null,
|
|
222
|
+
updated: null,
|
|
223
|
+
expires: null,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
let events: any = [];
|
|
227
|
+
|
|
228
|
+
if (Array.isArray(values)) {
|
|
229
|
+
events = values;
|
|
230
|
+
} else if (typeof values === "object") {
|
|
231
|
+
events = Object.values(values);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const [event, field] of eventMap) {
|
|
235
|
+
const date = events.find((ev: any) => ev.eventAction === event);
|
|
236
|
+
if (date?.eventDate) {
|
|
237
|
+
ts[field] = new Date(date.eventDate);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return ts;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// await whois(process.argv[2]).then((r) =>
|
|
245
|
+
// console.log(JSON.stringify(r, null, 2))
|
|
246
|
+
// );
|
package/src/port43.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ParseResultType, parseDomain } from "parse-domain";
|
|
2
|
+
import { PromiseSocket } from "promise-socket";
|
|
3
|
+
import { Socket } from "net";
|
|
4
|
+
import { port43servers, port43parsers } from "./port43servers.js";
|
|
5
|
+
import { ianaToRegistrarCache } from "./utils/ianaIdToRegistrar.js";
|
|
6
|
+
import { WhoisResponse } from "../whois.js";
|
|
7
|
+
import { normalizeWhoisStatus } from "./whoisStatus.js";
|
|
8
|
+
|
|
9
|
+
export function determinePort43Domain(actor: string) {
|
|
10
|
+
const parsed = parseDomain(actor);
|
|
11
|
+
|
|
12
|
+
if (parsed.type === ParseResultType.Listed) {
|
|
13
|
+
let tld = parsed.topLevelDomains.join(".");
|
|
14
|
+
if (port43servers[tld]) {
|
|
15
|
+
const domain = parsed.domain + "." + tld;
|
|
16
|
+
return [domain, tld, port43servers[tld]];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
tld = parsed.icann.topLevelDomains.join(".");
|
|
20
|
+
if (port43servers[tld]) {
|
|
21
|
+
const domain = parsed.icann.domain + "." + tld;
|
|
22
|
+
return [domain, tld, port43servers[tld]];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [actor, "", null];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function port43(actor: string): Promise<WhoisResponse> {
|
|
30
|
+
const [domain, tld, whoisServer] = determinePort43Domain(actor);
|
|
31
|
+
const opts = whoisServer;
|
|
32
|
+
const server = opts?.host || opts || null;
|
|
33
|
+
const query = opts?.query
|
|
34
|
+
? opts.query.replace("$addr", domain)
|
|
35
|
+
: `${domain}\r\n`;
|
|
36
|
+
const port = opts?.port || 43;
|
|
37
|
+
|
|
38
|
+
// console.log(`looking up ${domain} on ${server}`);
|
|
39
|
+
|
|
40
|
+
const response: WhoisResponse = {
|
|
41
|
+
found: true,
|
|
42
|
+
registrar: { id: 0, name: null },
|
|
43
|
+
reseller: null,
|
|
44
|
+
status: [],
|
|
45
|
+
nameservers: [],
|
|
46
|
+
ts: { created: null, updated: null, expires: null },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (!server) {
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let port43response = "";
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const promiseSocket = new PromiseSocket(new Socket());
|
|
57
|
+
promiseSocket.setTimeout(5 * 1000);
|
|
58
|
+
await promiseSocket.connect(port, server);
|
|
59
|
+
await promiseSocket.write(query);
|
|
60
|
+
port43response = (await promiseSocket.readAll())!
|
|
61
|
+
.toString()
|
|
62
|
+
.replace(/^[ \t]+/gm, "");
|
|
63
|
+
await promiseSocket.end();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
response.found = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!response.found) {
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
port43response.match(
|
|
74
|
+
/^%*\s+(NOT FOUND|No match|NO OBJECT FOUND|No entries found|No Data Found|No information available|Status: free)\b/im
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
response.found = false;
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let m;
|
|
82
|
+
|
|
83
|
+
if (port43parsers[tld]) {
|
|
84
|
+
port43parsers[tld](port43response, response);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
!response.registrar.name &&
|
|
88
|
+
(m = port43response.match(
|
|
89
|
+
/^(?:Registrar(?: Name)?|registrar_name|registrar):[ \t]*(\S.+)/m
|
|
90
|
+
)) &&
|
|
91
|
+
(response.registrar.name = m[1]);
|
|
92
|
+
!response.reseller &&
|
|
93
|
+
(m = port43response.match(
|
|
94
|
+
/^(?:Reseller(?: Name)?|reseller_name|reseller):[ \t]*(\S.+)/m
|
|
95
|
+
)) &&
|
|
96
|
+
(response.reseller = m[1]);
|
|
97
|
+
!response.registrar.id &&
|
|
98
|
+
(m = port43response.match(/^Registrar IANA ID:[ \t]*(\d+)/m)) &&
|
|
99
|
+
(response.registrar.id = parseInt(m[1] || "0"));
|
|
100
|
+
!response.ts.updated &&
|
|
101
|
+
(m = port43response.match(
|
|
102
|
+
/^(?:Last Modified|Updated Date|domain_datelastmodified|last-update):[ \t]*(\S.+)/m
|
|
103
|
+
)) &&
|
|
104
|
+
(response.ts.updated = new Date(m[1]) || null);
|
|
105
|
+
!response.ts.created &&
|
|
106
|
+
(m = port43response.match(
|
|
107
|
+
/^(?:Creation Date|domain_dateregistered|created):[ \t]*(\S.+)/m
|
|
108
|
+
)) &&
|
|
109
|
+
(response.ts.created = new Date(m[1]) || null);
|
|
110
|
+
!response.ts.expires &&
|
|
111
|
+
(m = port43response.match(
|
|
112
|
+
/^(?:(?:Registry )?Expiry Date):[ \t]*(\S.+)/m
|
|
113
|
+
)) &&
|
|
114
|
+
(response.ts.expires = new Date(m[1]) || null);
|
|
115
|
+
(m = port43response.match(/^(?:Status|Domain Status|status):.*/gm)) &&
|
|
116
|
+
m.forEach((s) => {
|
|
117
|
+
let m;
|
|
118
|
+
(m = s.match(
|
|
119
|
+
/^(?:Status|Domain Status|status):[ \t]*(?:<a[^>]*>)?(\S+)/m
|
|
120
|
+
)) && response.status.push(normalizeWhoisStatus(m[1]));
|
|
121
|
+
});
|
|
122
|
+
(m = port43response.match(
|
|
123
|
+
/^(?:Name Server|ns_name_\d+|namserver|nserver):.*/gm
|
|
124
|
+
)) &&
|
|
125
|
+
m.forEach((s) => {
|
|
126
|
+
let m;
|
|
127
|
+
(m = s.match(
|
|
128
|
+
/^(?:Name Server|ns_name_\d+|namserver|nserver):[ \t]*(.*)/m
|
|
129
|
+
)) && response.nameservers.push(m[1].toLowerCase());
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
133
|
+
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
134
|
+
if (name === response.registrar.name) {
|
|
135
|
+
response.registrar.id = id;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (response.registrar.id === 0 && response.registrar.name !== "") {
|
|
142
|
+
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
143
|
+
if (name.match(new RegExp(`\\b${response.registrar.name}\\b`, "i"))) {
|
|
144
|
+
response.registrar.id = id;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (response.registrar.id === 0 && response.registrar.name) {
|
|
151
|
+
for (const [id, { name }] of ianaToRegistrarCache.entries()) {
|
|
152
|
+
if (
|
|
153
|
+
name.match(
|
|
154
|
+
new RegExp(`\\b${response.registrar.name.replace(/,.*/, "")}\\b`, "i")
|
|
155
|
+
)
|
|
156
|
+
) {
|
|
157
|
+
response.registrar.id = id;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return response;
|
|
164
|
+
}
|