@stacksjs/whois 0.70.53 → 0.70.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/whois",
3
3
  "type": "module",
4
- "version": "0.70.53",
4
+ "version": "0.70.55",
5
5
  "description": "Easily get whois info.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -41,7 +41,8 @@
41
41
  "types": "./dist/index.d.ts",
42
42
  "files": [
43
43
  "README.md",
44
- "dist"
44
+ "dist",
45
+ "src"
45
46
  ],
46
47
  "scripts": {
47
48
  "build": "bun build.ts",
@@ -0,0 +1,325 @@
1
+ export const IANA_CHK_URL: string = 'https://www.iana.org/whois?q='
2
+
3
+ export const PARAMETERS: Record<string, string> = {
4
+ 'whois.denic.de': '-T dn,ace',
5
+ 'whois.nic.fr': '-V Md5.2',
6
+ }
7
+
8
+ export const SERVERS: Record<string, string> = {
9
+ 'br.com': 'whois.centralnic.net',
10
+ 'cn.com': 'whois.centralnic.net',
11
+ 'de.com': 'whois.centralnic.net',
12
+ 'eu.com': 'whois.centralnic.net',
13
+ 'gb.com': 'whois.centralnic.net',
14
+ 'gb.net': 'whois.centralnic.net',
15
+ 'gr.com': 'whois.centralnic.net',
16
+ 'hu.com': 'whois.centralnic.net',
17
+ 'in.net': 'whois.centralnic.net',
18
+ 'no.com': 'whois.centralnic.net',
19
+ 'qc.com': 'whois.centralnic.net',
20
+ 'ru.com': 'whois.centralnic.net',
21
+ 'sa.com': 'whois.centralnic.net',
22
+ 'se.com': 'whois.centralnic.net',
23
+ 'se.net': 'whois.centralnic.net',
24
+ 'uk.com': 'whois.centralnic.net',
25
+ 'uk.net': 'whois.centralnic.net',
26
+ 'us.com': 'whois.centralnic.net',
27
+ 'uy.com': 'whois.centralnic.net',
28
+ 'za.com': 'whois.centralnic.net',
29
+ 'jpn.com': 'whois.centralnic.ne',
30
+ 'web.com': 'whois.centralnic.ne',
31
+ 'com': 'whois.verisign-grs.com',
32
+ 'za.net': 'whois.za.net',
33
+ 'net': 'whois.verisign-grs.com',
34
+ 'eu.org': 'whois.eu.org',
35
+ 'za.org': 'whois.za.org',
36
+ 'org': 'whois.pir.org',
37
+ 'llyw.cymru': 'whois.nic.llyw.cymru',
38
+ 'gov.scot': 'whois.nic.gov.scot',
39
+ 'gov.wales': 'whois.nic.gov.wales',
40
+ 'edu': 'whois.educause.edu',
41
+ 'gov': 'whois.dotgov.gov',
42
+ 'int': 'whois.iana.org',
43
+ 'e164.arpa': 'whois.ripe.net',
44
+ 'arpa': 'whois.iana.org',
45
+ 'aero': 'whois.aero',
46
+ 'asia': 'whois.nic.asia',
47
+ 'biz': 'whois.nic.biz',
48
+ 'cat': 'whois.nic.cat',
49
+ 'coop': 'whois.nic.coop',
50
+ 'info': 'whois.afilias.net',
51
+ 'jobs': 'whois.nic.jobs',
52
+ 'mobi': 'whois.afilias.net',
53
+ 'museum': 'whois.nic.museum',
54
+ 'name': 'whois.nic.name',
55
+ 'post': 'whois.dotpostregistry.net',
56
+ 'pro': 'whois.nic.pro',
57
+ 'tel': 'whois.nic.tel',
58
+ 'travel': 'whois.nic.travel',
59
+ 'xxx': 'whois.nic.xxx',
60
+ 'ac': 'whois.nic.ac',
61
+ 'ae': 'whois.aeda.net.ae',
62
+ 'af': 'whois.nic.af',
63
+ 'ag': 'whois.nic.ag',
64
+ 'ai': 'whois.nic.ai',
65
+ 'am': 'whois.amnic.net',
66
+ 'ar': 'whois.nic.ar',
67
+ 'as': 'whois.nic.as',
68
+ 'priv.at': 'whois.nic.priv.at',
69
+ 'at': 'whois.nic.at',
70
+ 'au': 'whois.auda.org.au',
71
+ 'aw': 'whois.nic.aw',
72
+ 'ax': 'whois.ax',
73
+ 'be': 'whois.dns.be',
74
+ 'bf': 'whois.registre.bf',
75
+ 'bg': 'whois.register.bg',
76
+ 'bh': 'whois.nic.bh',
77
+ 'bi': 'whois1.nic.bi',
78
+ 'bj': 'whois.nic.bj',
79
+ 'bm': 'whois.afilias-srs.net',
80
+ 'bn': 'whois.bnnic.bn',
81
+ 'bo': 'whois.nic.bo',
82
+ 'br': 'whois.registro.br',
83
+ 'by': 'whois.cctld.by',
84
+ 'bw': 'whois.nic.net.bw',
85
+ 'bz': 'whois.afilias-grs.info',
86
+ 'co.ca': 'whois.co.ca',
87
+ 'ca': 'whois.cira.ca',
88
+ 'cc': 'ccwhois.verisign-grs.com',
89
+ 'cd': 'whois.nic.cd',
90
+ 'ch': 'whois.nic.ch',
91
+ 'ci': 'whois.nic.ci',
92
+ 'cl': 'whois.nic.cl',
93
+ 'cm': 'whois.netcom.cm',
94
+ 'edu.cn': 'whois.edu.cn',
95
+ 'cn': 'whois.cnnic.cn',
96
+ 'uk.co': 'whois.uk.co',
97
+ 'co': 'whois.nic.co',
98
+ 'cr': 'whois.nic.cr',
99
+ 'cx': 'whois.nic.cx',
100
+ 'cz': 'whois.nic.cz',
101
+ 'de': 'whois.denic.de',
102
+ 'dk': 'whois.dk-hostmaster.dk',
103
+ 'dm': 'whois.dmdomains.dm',
104
+ 'do': 'whois.nic.do',
105
+ 'dz': 'whois.nic.dz',
106
+ 'ec': 'whois.nic.ec',
107
+ 'ee': 'whois.tld.ee',
108
+ 'eu': 'whois.eu',
109
+ 'fi': 'whois.fi',
110
+ 'fj': 'www.whois.fj',
111
+ 'fm': 'whois.nic.fm',
112
+ 'fo': 'whois.nic.fo',
113
+ 'fr': 'whois.nic.fr',
114
+ 'gd': 'whois.nic.gd',
115
+ 'ge': 'whois.nic.ge',
116
+ 'gf': 'whois.mediaserv.net',
117
+ 'gg': 'whois.gg',
118
+ 'gh': 'whois.nic.gh',
119
+ 'gi': 'whois2.afilias-grs.net',
120
+ 'gl': 'whois.nic.gl',
121
+ 'gp': 'whois.nic.gp',
122
+ 'gq': 'whois.dominio.gq',
123
+ 'gs': 'whois.nic.gs',
124
+ 'gy': 'whois.registry.gy',
125
+ 'hk': 'whois.hkirc.hk',
126
+ 'hm': 'whois.registry.hm',
127
+ 'hn': 'whois.nic.hn',
128
+ 'hr': 'whois.dns.hr',
129
+ 'ht': 'whois.nic.ht',
130
+ 'hu': 'whois.nic.hu',
131
+ 'id': 'whois.id',
132
+ 'ie': 'whois.weare.ie',
133
+ 'il': 'whois.isoc.org.il',
134
+ 'im': 'whois.nic.im',
135
+ 'in': 'whois.registry.in',
136
+ 'io': 'whois.nic.io',
137
+ 'iq': 'whois.cmc.iq',
138
+ 'ir': 'whois.nic.ir',
139
+ 'is': 'whois.isnic.is',
140
+ 'it': 'whois.nic.it',
141
+ 'je': 'whois.je',
142
+ 'jp': 'whois.jprs.jp',
143
+ 'ke': 'whois.kenic.or.ke',
144
+ 'kg': 'whois.kg',
145
+ 'ki': 'whois.nic.ki',
146
+ 'kn': 'whois.nic.kn',
147
+ 'kr': 'whois.kr',
148
+ 'kw': 'whois.nic.kw',
149
+ 'ky': 'whois.kyregistry.ky',
150
+ 'kz': 'whois.nic.kz',
151
+ 'la': 'whois.nic.la',
152
+ 'lb': 'whois.lbdr.org.lb',
153
+ 'lc': 'whois2.afilias-grs.net',
154
+ 'li': 'whois.nic.li',
155
+ 'lk': 'whois.nic.lk',
156
+ 'ls': 'whois.nic.ls',
157
+ 'lt': 'whois.domreg.lt',
158
+ 'lu': 'whois.dns.lu',
159
+ 'lv': 'whois.nic.lv',
160
+ 'ly': 'whois.nic.ly',
161
+ 'ma': 'whois.registre.ma',
162
+ 'md': 'whois.nic.md',
163
+ 'me': 'whois.nic.me',
164
+ 'mg': 'whois.nic.mg',
165
+ 'mk': 'whois.marnet.mk',
166
+ 'ml': 'whois.dot.ml',
167
+ 'mm': 'whois.registry.gov.mm',
168
+ 'mn': 'whois.nic.mn',
169
+ 'mq': 'whois.mediaserv.net',
170
+ 'mr': 'whois.nic.mr',
171
+ 'ms': 'whois.nic.ms',
172
+ 'mt': 'whois.nic.org.mt',
173
+ 'mu': 'whois.nic.mu',
174
+ 'mw': 'whois.nic.mw',
175
+ 'mx': 'whois.mx',
176
+ 'my': 'whois.mynic.my',
177
+ 'mz': 'whois.nic.mz',
178
+ 'na': 'whois.na-nic.com.na',
179
+ 'nc': 'whois.nc',
180
+ 'nf': 'whois.nic.nf',
181
+ 'ng': 'whois.nic.net.ng',
182
+ 'nl': 'whois.domain-registry.nl',
183
+ 'no': 'whois.norid.no',
184
+ 'nu': 'whois.iis.nu',
185
+ 'nz': 'whois.irs.net.nz',
186
+ 'om': 'whois.registry.om',
187
+ 'pe': 'kero.yachay.pe',
188
+ 'pf': 'whois.registry.pf',
189
+ 'pk': 'whois.pknic.net.pk',
190
+ 'co.pl': 'whois.co.pl',
191
+ 'pl': 'whois.dns.pl',
192
+ 'pm': 'whois.nic.pm',
193
+ 'pr': 'whois.afilias-srs.net',
194
+ 'ps': 'whois.pnina.ps',
195
+ 'pt': 'whois.dns.pt',
196
+ 'pw': 'whois.nic.pw',
197
+ 'qa': 'whois.registry.qa',
198
+ 're': 'whois.nic.re',
199
+ 'ro': 'whois.rotld.ro',
200
+ 'rs': 'whois.rnids.rs',
201
+ 'ac.ru': 'whois.free.net',
202
+ 'edu.ru': 'whois.informika.ru',
203
+ 'com.ru': 'whois.flexireg.net',
204
+ 'msk.ru': 'whois.flexireg.net',
205
+ 'net.ru': 'whois.nic.net.ru',
206
+ 'nov.ru': 'whois.flexireg.net',
207
+ 'org.ru': 'whois.nic.net.ru',
208
+ 'pp.ru': 'whois.nic.net.ru',
209
+ 'spb.ru': 'whois.flexireg.net',
210
+ 'ru': 'whois.tcinet.ru',
211
+ 'rw': 'whois.ricta.org.rw',
212
+ 'sa': 'whois.nic.net.sa',
213
+ 'sb': 'whois.nic.net.sb',
214
+ 'sc': 'whois2.afilias-grs.net',
215
+ 'sd': 'whois.sdnic.sd',
216
+ 'se': 'whois.iis.se',
217
+ 'sg': 'whois.sgnic.sg',
218
+ 'sh': 'whois.nic.sh',
219
+ 'si': 'whois.register.si',
220
+ 'sk': 'whois.sk-nic.sk',
221
+ 'sl': 'whois.nic.sl',
222
+ 'sm': 'whois.nic.sm',
223
+ 'sn': 'whois.nic.sn',
224
+ 'so': 'whois.nic.so',
225
+ 'ss': 'whois.nic.ss',
226
+ 'st': 'whois.nic.st',
227
+ 'msk.su': 'whois.flexireg.net',
228
+ 'nov.su': 'whois.flexireg.net',
229
+ 'spb.su': 'whois.flexireg.net',
230
+ 'su': 'whois.tcinet.ru',
231
+ 'sx': 'whois.sx',
232
+ 'sy': 'whois.tld.sy',
233
+ 'tc': 'whois.nic.tc',
234
+ 'td': 'whois.nic.td',
235
+ 'tf': 'whois.nic.tf',
236
+ 'tg': 'whois.nic.tg',
237
+ 'th': 'whois.thnic.co.th',
238
+ 'tk': 'whois.dot.tk',
239
+ 'tl': 'whois.nic.tl',
240
+ 'tm': 'whois.nic.tm',
241
+ 'tn': 'whois.ati.tn',
242
+ 'to': 'whois.tonic.to',
243
+ 'tr': 'whois.trabis.gov.tr',
244
+ 'tv': 'whois.nic.tv',
245
+ 'tw': 'whois.twnic.net.tw',
246
+ 'tz': 'whois.tznic.or.tz',
247
+ 'biz.ua': 'whois.biz.ua',
248
+ 'co.ua': 'whois.co.ua',
249
+ 'pp.ua': 'whois.pp.ua',
250
+ 'ua': 'whois.ua',
251
+ 'ug': 'whois.co.ug',
252
+ 'ac.uk': 'whois.nic.ac.uk',
253
+ 'gov.uk': 'whois.gov.uk',
254
+ 'uk': 'whois.nic.uk',
255
+ 'fed.us': 'whois.nic.gov',
256
+ 'us': 'whois.nic.us',
257
+ 'uy': 'whois.nic.org.uy',
258
+ 'uz': 'whois.cctld.uz',
259
+ 'vc': 'whois2.afilias-grs.net',
260
+ 've': 'whois.nic.ve',
261
+ 'vg': 'whois.nic.vg',
262
+ 'vu': 'whois.dnrs.vu',
263
+ 'wf': 'whois.nic.wf',
264
+ 'ws': 'whois.website.ws',
265
+ 'yt': 'whois.nic.yt',
266
+ 'ac.za': 'whois.ac.za',
267
+ 'co.za': 'whois.registry.net.za',
268
+ 'gov.za': 'whois.gov.za',
269
+ 'net.za': 'net-whois.registry.net.za',
270
+ 'org.za': 'org-whois.registry.net.za',
271
+ 'web.za': 'web-whois.registry.net.za',
272
+ 'zm': 'whois.zicta.zm',
273
+ 'xn--2scrj9c': 'whois.registry.in',
274
+ 'xn--3e0b707e': 'whois.kr',
275
+ 'xn--3hcrj9c': 'whois.registry.in',
276
+ 'xn--45br5cyl': 'whois.registry.in',
277
+ 'xn--45brj9c': 'whois.registry.in',
278
+ 'xn--4dbrk0ce': 'whois.isoc.org.il',
279
+ 'xn--80ao21a': 'whois.nic.kz',
280
+ 'xn--90a3ac': 'whois.rnids.rs',
281
+ 'xn--90ae': 'whois.imena.bg',
282
+ 'xn--90ais': 'whois.cctld.by',
283
+ 'xn--clchc0ea0b2g2a9gcd': 'whois.sgnic.sg',
284
+ 'xn--d1alf': 'whois.marnet.mk',
285
+ 'xn--e1a4c': 'whois.eu',
286
+ 'xn--fiqs8s': 'cwhois.cnnic.cn',
287
+ 'xn--fiqz9s': 'cwhois.cnnic.cn',
288
+ 'xn--fpcrj9c3d': 'whois.registry.in',
289
+ 'xn--fzc2c9e2c': 'whois.nic.lk',
290
+ 'xn--gecrj9c': 'whois.registry.in',
291
+ 'xn--h2breg3eve': 'whois.registry.in',
292
+ 'xn--h2brj9c8c': 'whois.registry.in',
293
+ 'xn--h2brj9c': 'whois.registry.in',
294
+ 'xn--j1amh': 'whois.dotukr.com',
295
+ 'xn--j6w193g': 'whois.hkirc.hk',
296
+ 'xn--kprw13d': 'whois.twnic.net.tw',
297
+ 'xn--kpry57d': 'whois.twnic.net.tw',
298
+ 'xn--lgbbat1ad8j': 'whois.nic.dz',
299
+ 'xn--mgb9awbf': 'whois.registry.om',
300
+ 'xn--mgba3a4f16a': 'whois.nic.ir',
301
+ 'xn--mgbaam7a8h': 'whois.aeda.net.ae',
302
+ 'xn--mgbah1a3hjkrd': 'whois.nic.mr',
303
+ 'xn--mgbbh1a71e': 'whois.registry.in',
304
+ 'xn--mgbbh1a': 'whois.registry.in',
305
+ 'xn--mgberp4a5d4ar': 'whois.nic.net.sa',
306
+ 'xn--mgbgu82a': 'whois.registry.in',
307
+ 'xn--mgbtx2b': 'whois.cmc.iq',
308
+ 'xn--mgbx4cd0ab': 'whois.mynic.my',
309
+ 'xn--node': 'whois.itdc.ge',
310
+ 'xn--o3cw4h': 'whois.thnic.co.th',
311
+ 'xn--ogbpf8fl': 'whois.tld.sy',
312
+ 'xn--p1ai': 'whois.tcinet.ru',
313
+ 'xn--pgbs0dh': 'whois.ati.tn',
314
+ 'xn--q7ce6a': 'whois.nic.la',
315
+ 'xn--qxa6a': 'whois.eu',
316
+ 'xn--rvc1e0am3e': 'whois.registry.in',
317
+ 'xn--s9brj9c': 'whois.registry.in',
318
+ 'xn--wgbh1c': 'whois.dotmasr.eg',
319
+ 'xn--wgbl6a': 'whois.registry.qa',
320
+ 'xn--xkc2al3hye2a': 'whois.nic.lk',
321
+ 'xn--xkc2dl3a5ee0h': 'whois.registry.in',
322
+ 'xn--y9a3aq': 'whois.amnic.net',
323
+ 'xn--yfro4i67o': 'whois.sgnic.sg',
324
+ 'xn--ygbi2ammx': 'whois.pnina.ps',
325
+ }
package/src/index.ts ADDED
@@ -0,0 +1,395 @@
1
+ import type { ProxyData, WhoIsOptions, WhoIsResponse } from './types'
2
+ import Net from 'node:net'
3
+ import { log } from '@stacksjs/logging'
4
+ import { IANA_CHK_URL, PARAMETERS, SERVERS } from './constants'
5
+ import { SocksClient } from './socks'
6
+ import type { SocksClientOptions } from './socks'
7
+ import { ProxyType } from './types'
8
+ import { shallowCopy } from './utils'
9
+
10
+ /**
11
+ * Find the WhoIs server for the TLD from IANA WhoIs service. The TLD is be searched and the HTML response is parsed to extract the WhoIs server
12
+ *
13
+ * @param tld TLD of the domain
14
+ * @returns WhoIs server which hosts the information for the domains of the TLD
15
+ */
16
+ export async function findWhoIsServer(tld: string): Promise<string> {
17
+ const chkURL = IANA_CHK_URL + tld
18
+
19
+ try {
20
+ const res = await fetch(chkURL)
21
+ if (res.ok) {
22
+ const body = await res.text()
23
+ const server = body.match(/whois:\s+(\S+)/)
24
+ if (server?.[1])
25
+ return server[1]
26
+ }
27
+ }
28
+ catch (err) {
29
+ console.error('Error in getting WhoIs server data from IANA', err)
30
+ }
31
+
32
+ return ''
33
+ }
34
+
35
+ /**
36
+ * Get whois server of the tld from servers list
37
+ *
38
+ * @param tld TLD of the domain
39
+ * @returns WhoIs server which hosts the information for the domains of the TLD
40
+ */
41
+ export function getWhoIsServer(tld: keyof typeof SERVERS): string | undefined {
42
+ if (tld === 'com')
43
+ return 'whois.verisign-grs.com'
44
+ return SERVERS[tld]
45
+ }
46
+
47
+ /**
48
+ * Extract TLD from domain name.
49
+ * If the TLD is in whois-servers.json file, then the TLD is returned.
50
+ * If TLD is not found within the file, then determined by taking the last element after splitting the domain name from '.'
51
+ *
52
+ * @param domain Domain name
53
+ * @returns TLD
54
+ */
55
+ export function getTLD(domain: string): keyof typeof SERVERS {
56
+ const domainParts = domain.split('.')
57
+ for (let i = domainParts.length - 1; i > 0; i--) {
58
+ const possibleTLD = domainParts.slice(i).join('.') as keyof typeof SERVERS
59
+ if (SERVERS[possibleTLD]) {
60
+ return possibleTLD
61
+ }
62
+ }
63
+ // If no match found in SERVERS, return the last part
64
+ return domainParts[domainParts.length - 1] as keyof typeof SERVERS
65
+ }
66
+
67
+ // get whois query parameters if exist on parameters.json for whois server
68
+ export function getParameters(server: string): string | undefined {
69
+ return (PARAMETERS as { [key: string]: string })[server]
70
+ }
71
+
72
+ /**
73
+ * Parse collected raw WhoIs data
74
+ *
75
+ * @class
76
+ */
77
+ export class WhoIsParser {
78
+ /**
79
+ * Iterated through the complete text and returns extracted values
80
+ *
81
+ * @param rawData raw text from WhoIs server
82
+ * @param outputData Data which needs to be extracted from the raw text (key/value pairs). Keys are used to extract from raw text and values are filled.
83
+ * @returns Filled {@link outputData}
84
+ */
85
+ private static iterParse(rawData: string, outputData: any) {
86
+ let lastStr = ''
87
+ let lastField: string | null = null
88
+ let lastLetter = ''
89
+
90
+ for (let i = 0; i < rawData.length; i++) {
91
+ const letter = rawData[i]
92
+ if (letter === '\n' || (lastLetter === ':' && letter === ' ')) {
93
+ if (lastStr.trim() in outputData) {
94
+ lastField = lastStr.trim()
95
+ }
96
+ else if (lastField !== null) {
97
+ const x = lastStr.trim()
98
+ if (x !== '') {
99
+ const obj = outputData[lastField]
100
+ if (Array.isArray(obj))
101
+ obj.push(x)
102
+ else outputData[lastField] = x
103
+
104
+ lastField = null
105
+ }
106
+ }
107
+ lastStr = ''
108
+ }
109
+ else if (letter !== ':') {
110
+ lastStr = lastStr + letter
111
+ }
112
+ lastLetter = letter as string
113
+ if (lastStr === 'Record maintained by' || lastStr === '>>>')
114
+ break
115
+ }
116
+
117
+ return outputData
118
+ }
119
+
120
+ /**
121
+ * Parse the raw WhoIs text and returns extracted values
122
+ *
123
+ * @param rawData raw text from WhoIs server
124
+ * @param outputData Data which needs to be extracted from the raw text (key/value pairs). Keys are used to extract from raw text and values are filled.
125
+ * @returns Filled {@link outputData}
126
+ */
127
+ public static parseData(rawData: string, outputData: any | null): any {
128
+ if (!outputData) {
129
+ outputData = {
130
+ 'Domain Name': '',
131
+ 'Creation Date': '',
132
+ 'Updated Date': '',
133
+ 'Registry Expiry Date': '',
134
+ 'Domain Status': [],
135
+ 'Registrar': '',
136
+ }
137
+ }
138
+
139
+ outputData = WhoIsParser.iterParse(rawData, outputData)
140
+
141
+ return outputData
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Connects to the provided {@link server}:{@link port} through TCP (through a proxy if a proxy is given), run the WhoIs query and returns the response
147
+ *
148
+ * @param domain Domain name
149
+ * @param queryOptions Query options which can be used with the specific WhoIs server to get the complete response
150
+ * @param server WhoIs server
151
+ * @param port WhoIs server port
152
+ * @param encoding Encoding used by the WhoIs server
153
+ * @param proxy {@link ProxyData}
154
+ * @returns The {string} WhoIs response for the query. Empty string is returned for errors
155
+ */
156
+ export async function tcpWhois(
157
+ domain: string,
158
+ queryOptions: string,
159
+ server: string,
160
+ port: number,
161
+ encoding: string,
162
+ proxy: ProxyData | null,
163
+ ): Promise<string> {
164
+ const decoder = new TextDecoder(encoding)
165
+ const encoder = new TextEncoder()
166
+
167
+ if (!proxy) {
168
+ const socket = new Net.Socket()
169
+ // 30s socket timeout — without this, an unresponsive WHOIS server
170
+ // (or a network partition) leaves the lookup hanging indefinitely
171
+ // and any caller awaiting it freezes alongside it.
172
+ socket.setTimeout(30_000)
173
+ return new Promise((resolve, reject) => {
174
+ try {
175
+ socket.connect({ port, host: server }, () => {
176
+ if (queryOptions !== '')
177
+ socket.write(encoder.encode(`${queryOptions} ${domain}\r\n`))
178
+ else socket.write(encoder.encode(`${domain}\r\n`))
179
+ })
180
+
181
+ socket.on('data', (data) => {
182
+ resolve(decoder.decode(data as any))
183
+ })
184
+
185
+ socket.on('error', (error) => {
186
+ reject(error)
187
+ })
188
+
189
+ socket.on('timeout', () => {
190
+ socket.destroy()
191
+ reject(new Error(`WHOIS request to ${server}:${port} timed out after 30s`))
192
+ })
193
+ }
194
+ catch (e) {
195
+ reject(e)
196
+ }
197
+ })
198
+ }
199
+
200
+ const options: SocksClientOptions = {
201
+ proxy: {
202
+ host: proxy.ip,
203
+ port: proxy.port,
204
+ type: proxy.type === ProxyType.SOCKS5 ? 5 : 4,
205
+ },
206
+
207
+ command: 'connect',
208
+
209
+ destination: {
210
+ host: server,
211
+ port,
212
+ },
213
+ }
214
+
215
+ if (proxy.username && proxy.password) {
216
+ options.proxy.userId = proxy.username
217
+ options.proxy.password = proxy.password
218
+ }
219
+
220
+ return new Promise((resolve, reject) => {
221
+ SocksClient.createConnection(options, (err: any, info: any) => {
222
+ if (err) {
223
+ reject(err)
224
+ }
225
+ else {
226
+ if (!info)
227
+ reject(new Error('No socket info received!'))
228
+
229
+ if (queryOptions !== '') {
230
+ info?.socket.write(encoder.encode(`${queryOptions} ${domain}\r\n`))
231
+ }
232
+ else {
233
+ info?.socket.write(encoder.encode(`${domain}\r\n`))
234
+ }
235
+
236
+ info?.socket.on('data', (data: any) => {
237
+ resolve(decoder.decode(data as any))
238
+ })
239
+
240
+ info?.socket.resume()
241
+ }
242
+ })
243
+ })
244
+ }
245
+
246
+ /**
247
+ * Collect WhoIs data for the mentioned {@link domain}. Parse the received response if {@link parse} is true, accordingly.
248
+ *
249
+ * @param domain Domain name
250
+ * @param parse Whether the raw text needs to be parsed/formatted or not
251
+ * @param options {@link WhoIsOptions}
252
+ * @returns {@link WhoIsResponse} Returns a {@link WhoIsResponse} object which contains the raw text and parsed data (if parse is true)
253
+ */
254
+ export async function whois(
255
+ domain: string,
256
+ parse = false,
257
+ options: WhoIsOptions | null = null,
258
+ ): Promise<WhoIsResponse> {
259
+ let tld: string
260
+ let port = 43
261
+ let server = ''
262
+ let proxy: ProxyData | null
263
+ let encoding = 'utf-8'
264
+
265
+ if (!options) {
266
+ tld = getTLD(domain)
267
+ proxy = null
268
+ }
269
+ else {
270
+ tld = options.tld ? options.tld : getTLD(domain)
271
+ encoding = options.encoding ? options.encoding : 'utf-8'
272
+ proxy = options.proxy ? options.proxy : null
273
+ server = options.server ? options.server : ''
274
+ port = options.serverPort ? options.serverPort : 43
275
+ }
276
+
277
+ if (server === '') {
278
+ let serverData = getWhoIsServer(tld as keyof typeof SERVERS)
279
+ if (!serverData) {
280
+ log.debug(`No WhoIs server found for TLD: ${tld}! Attempting IANA WhoIs database for server!`)
281
+ serverData = await findWhoIsServer(tld)
282
+ if (!serverData) {
283
+ log.debug('WhoIs server could not be found!')
284
+ return {
285
+ _raw: '',
286
+ parsedData: null,
287
+ }
288
+ }
289
+ log.debug(`WhoIs sever found for ${tld}: ${server}`)
290
+ }
291
+
292
+ server = serverData
293
+ }
294
+
295
+ const qOptions = getParameters(server)
296
+ const queryOptions = qOptions || ''
297
+
298
+ try {
299
+ log.debug(`Attempting WHOIS lookup for ${domain} on server ${server}`)
300
+ const rawData = await tcpWhois(domain, queryOptions, server, port, encoding, proxy)
301
+ log.debug(`Raw WHOIS data received:`, rawData)
302
+ if (!parse) {
303
+ const parsedData = WhoIsParser.parseData(rawData, null)
304
+ return {
305
+ _raw: rawData,
306
+ parsedData,
307
+ }
308
+ }
309
+
310
+ let outputData: any | null = null
311
+ if (options?.parseData)
312
+ outputData = shallowCopy(options.parseData)
313
+
314
+ try {
315
+ const parsedData = WhoIsParser.parseData(rawData, outputData)
316
+ return {
317
+ _raw: rawData,
318
+ parsedData,
319
+ }
320
+ }
321
+ catch (err) {
322
+ console.error('Error in parsing WhoIs data!', err)
323
+ return {
324
+ _raw: rawData,
325
+ parsedData: null,
326
+ }
327
+ }
328
+ }
329
+ catch (err: any) {
330
+ log.debug(`Error in WHOIS lookup for ${domain} on server ${server}`, err)
331
+ return {
332
+ _raw: '',
333
+ parsedData: null,
334
+ }
335
+ }
336
+ }
337
+
338
+ export function lookup(domain: string, options: WhoIsOptions | null = null): Promise<WhoIsResponse> {
339
+ log.debug(`Lookup called for ${domain}`)
340
+ return whois(domain, true, options)
341
+ }
342
+
343
+ /**
344
+ * Collects (and parse/format if set to be true) for the provided {@link domains}. If {@link parallel} is set to be true, multiple threads will be used to batch process the domains according to {@link threads} mentioned.
345
+ * If <i>options.parsedData</i> is mentioned, then it will be used to parse <b>all</b> the responses.
346
+ * If a proxy is mentioned in {@link options}, then the proxy will be used to collect <b>all</b> the WhoIs data.
347
+ *
348
+ * @param domains Domains Names
349
+ * @param parallel Whether data should be collected parallally or not
350
+ * @param threads Batch size (for parallel processing)
351
+ * @param parse Whether the raw text needs to be parsed/formatted or not
352
+ * @param options {@link WhoIsOptions}
353
+ * @returns Array of {@link WhoIsResponse} for all the domains. Order is not guaranteed
354
+ */
355
+ export async function batchWhois(
356
+ domains: string[],
357
+ parallel = false,
358
+ threads = 1,
359
+ parse = false,
360
+ options: WhoIsOptions | null = null,
361
+ ): Promise<WhoIsResponse[]> {
362
+ let response: WhoIsResponse[] = []
363
+
364
+ if (parallel) {
365
+ if (threads > domains.length)
366
+ threads = domains.length
367
+
368
+ for (let i = 0; i < domains.length; i += threads) {
369
+ const batch = domains.slice(i, i + threads)
370
+ // Accumulate batch results — the previous `response = await Promise.all(...)`
371
+ // overwrote the array each iteration, so a 100-domain batch with
372
+ // threads=10 returned only the last 10 entries instead of all 100.
373
+ const batchResults = await Promise.all(
374
+ batch.map(async (domain) => {
375
+ return await whois(domain, parse, options)
376
+ }),
377
+ )
378
+ response.push(...batchResults)
379
+ }
380
+ }
381
+ else {
382
+ for (let i = 0; i < domains.length; i++) {
383
+ const res = await whois(domains[i] as string, parse, options)
384
+ response.push(res)
385
+ }
386
+ }
387
+
388
+ return response
389
+ }
390
+
391
+ export * from './constants'
392
+ export * from './types'
393
+
394
+ export { SocksClient } from './socks'
395
+ export type { SocksClientOptions } from './socks'
package/src/socks.ts ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Native SOCKS client implementation using Bun's TCP APIs
3
+ * Replaces the 'socks' npm package
4
+ */
5
+
6
+ import Net from 'node:net'
7
+
8
+ export enum SocksVersion {
9
+ SOCKS4 = 4,
10
+ SOCKS5 = 5,
11
+ }
12
+
13
+ export interface SocksProxy {
14
+ host: string
15
+ port: number
16
+ type: 4 | 5
17
+ userId?: string
18
+ password?: string
19
+ }
20
+
21
+ export interface SocksDestination {
22
+ host: string
23
+ port: number
24
+ }
25
+
26
+ export interface SocksClientOptions {
27
+ proxy: SocksProxy
28
+ command: 'connect' | 'bind' | 'associate'
29
+ destination: SocksDestination
30
+ }
31
+
32
+ export interface SocksClientEstablishedEvent {
33
+ socket: Net.Socket
34
+ remoteHost?: string
35
+ remotePort?: number
36
+ }
37
+
38
+ /**
39
+ * Native SOCKS client implementation
40
+ */
41
+ export class SocksClient {
42
+ private options: SocksClientOptions
43
+
44
+ constructor(options: SocksClientOptions) {
45
+ this.options = options
46
+ }
47
+
48
+ /**
49
+ * Create a SOCKS connection
50
+ *
51
+ * @param options SOCKS connection options
52
+ * @param callback Callback with error or connection info
53
+ */
54
+ static createConnection(
55
+ options: SocksClientOptions,
56
+ callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void,
57
+ ): void {
58
+ const client = new SocksClient(options)
59
+ client.connect(callback)
60
+ }
61
+
62
+ /**
63
+ * Establish SOCKS connection
64
+ */
65
+ private connect(callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void): void {
66
+ const socket = new Net.Socket()
67
+ const { proxy, destination } = this.options
68
+
69
+ socket.connect({ port: proxy.port, host: proxy.host }, () => {
70
+ if (proxy.type === 5) {
71
+ this.connectSocks5(socket, callback)
72
+ }
73
+ else {
74
+ this.connectSocks4(socket, callback)
75
+ }
76
+ })
77
+
78
+ socket.on('error', (err) => {
79
+ callback(err)
80
+ })
81
+ }
82
+
83
+ /**
84
+ * SOCKS4 connection handshake
85
+ */
86
+ private connectSocks4(socket: Net.Socket, callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void): void {
87
+ const { destination, proxy } = this.options
88
+
89
+ // SOCKS4 request format:
90
+ // +----+----+----+----+----+----+----+----+----+----+....+----+
91
+ // | VN | CD | DSTPORT | DSTIP | USERID |NULL|
92
+ // +----+----+----+----+----+----+----+----+----+----+....+----+
93
+ // 1 1 2 4 variable 1
94
+
95
+ const port = destination.port
96
+ const userId = proxy.userId || ''
97
+
98
+ // Parse IP address
99
+ let ipBuffer: Buffer
100
+ if (this.isIPv4(destination.host)) {
101
+ const parts = destination.host.split('.').map(Number)
102
+ ipBuffer = Buffer.from(parts)
103
+ }
104
+ else {
105
+ // SOCKS4 doesn't support domain names, use SOCKS4a
106
+ ipBuffer = Buffer.from([0, 0, 0, 1]) // Special marker for SOCKS4a
107
+ }
108
+
109
+ const request = Buffer.concat([
110
+ Buffer.from([0x04]), // VN (SOCKS version 4)
111
+ Buffer.from([0x01]), // CD (CONNECT command)
112
+ Buffer.from([(port >> 8) & 0xFF, port & 0xFF]), // DSTPORT (2 bytes)
113
+ ipBuffer, // DSTIP (4 bytes)
114
+ Buffer.from(userId), // USERID
115
+ Buffer.from([0x00]), // NULL terminator
116
+ ])
117
+
118
+ // For SOCKS4a with domain name
119
+ if (!this.isIPv4(destination.host)) {
120
+ const domainBuffer = Buffer.concat([
121
+ request,
122
+ Buffer.from(destination.host),
123
+ Buffer.from([0x00]), // NULL terminator
124
+ ])
125
+ socket.write(domainBuffer)
126
+ }
127
+ else {
128
+ socket.write(request)
129
+ }
130
+
131
+ // Read response
132
+ socket.once('data', (data) => {
133
+ // SOCKS4 response format:
134
+ // +----+----+----+----+----+----+----+----+
135
+ // | VN | CD | DSTPORT | DSTIP |
136
+ // +----+----+----+----+----+----+----+----+
137
+ // 1 1 2 4
138
+
139
+ if (data.length < 8) {
140
+ callback(new Error('Invalid SOCKS4 response'))
141
+ return
142
+ }
143
+
144
+ const status = data[1]
145
+ if (status !== 0x5A) {
146
+ // 0x5A = request granted
147
+ callback(new Error(`SOCKS4 connection failed with status: ${status}`))
148
+ return
149
+ }
150
+
151
+ callback(null, { socket })
152
+ })
153
+ }
154
+
155
+ /**
156
+ * SOCKS5 connection handshake
157
+ */
158
+ private connectSocks5(socket: Net.Socket, callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void): void {
159
+ const { destination, proxy } = this.options
160
+
161
+ // Step 1: Authentication method negotiation
162
+ let authMethods: number[]
163
+ if (proxy.userId && proxy.password) {
164
+ authMethods = [0x00, 0x02] // No auth and username/password auth
165
+ }
166
+ else {
167
+ authMethods = [0x00] // No authentication
168
+ }
169
+
170
+ const greeting = Buffer.from([
171
+ 0x05, // SOCKS version
172
+ authMethods.length, // Number of auth methods
173
+ ...authMethods,
174
+ ])
175
+
176
+ socket.write(greeting)
177
+
178
+ socket.once('data', (data) => {
179
+ if (data.length < 2 || data[0] !== 0x05) {
180
+ callback(new Error('Invalid SOCKS5 greeting response'))
181
+ return
182
+ }
183
+
184
+ const selectedMethod = data[1]
185
+
186
+ if (selectedMethod === 0xFF) {
187
+ callback(new Error('No acceptable authentication methods'))
188
+ return
189
+ }
190
+
191
+ // Step 2: Authentication (if required)
192
+ if (selectedMethod === 0x02) {
193
+ this.authenticateSocks5(socket, callback)
194
+ }
195
+ else {
196
+ this.sendConnectRequest(socket, callback)
197
+ }
198
+ })
199
+ }
200
+
201
+ /**
202
+ * SOCKS5 username/password authentication
203
+ */
204
+ private authenticateSocks5(socket: Net.Socket, callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void): void {
205
+ const { proxy } = this.options
206
+ const username = proxy.userId || ''
207
+ const password = proxy.password || ''
208
+
209
+ const authRequest = Buffer.concat([
210
+ Buffer.from([0x01]), // Auth version
211
+ Buffer.from([username.length]),
212
+ Buffer.from(username),
213
+ Buffer.from([password.length]),
214
+ Buffer.from(password),
215
+ ])
216
+
217
+ socket.write(authRequest)
218
+
219
+ socket.once('data', (data) => {
220
+ if (data.length < 2 || data[0] !== 0x01) {
221
+ callback(new Error('Invalid SOCKS5 auth response'))
222
+ return
223
+ }
224
+
225
+ if (data[1] !== 0x00) {
226
+ callback(new Error('SOCKS5 authentication failed'))
227
+ return
228
+ }
229
+
230
+ this.sendConnectRequest(socket, callback)
231
+ })
232
+ }
233
+
234
+ /**
235
+ * Send SOCKS5 CONNECT request
236
+ */
237
+ private sendConnectRequest(socket: Net.Socket, callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void): void {
238
+ const { destination } = this.options
239
+ const port = destination.port
240
+
241
+ let addressBuffer: Buffer
242
+ let addressType: number
243
+
244
+ if (this.isIPv4(destination.host)) {
245
+ // IPv4 address
246
+ addressType = 0x01
247
+ const parts = destination.host.split('.').map(Number)
248
+ addressBuffer = Buffer.from(parts)
249
+ }
250
+ else if (this.isIPv6(destination.host)) {
251
+ // IPv6 address
252
+ addressType = 0x04
253
+ // Parse IPv6 (simplified)
254
+ const parts = destination.host.split(':')
255
+ const bytes: number[] = []
256
+ for (const part of parts) {
257
+ const value = Number.parseInt(part, 16)
258
+ bytes.push((value >> 8) & 0xFF, value & 0xFF)
259
+ }
260
+ addressBuffer = Buffer.from(bytes)
261
+ }
262
+ else {
263
+ // Domain name
264
+ addressType = 0x03
265
+ addressBuffer = Buffer.concat([
266
+ Buffer.from([destination.host.length]),
267
+ Buffer.from(destination.host),
268
+ ])
269
+ }
270
+
271
+ const request = Buffer.concat([
272
+ Buffer.from([
273
+ 0x05, // SOCKS version
274
+ 0x01, // CONNECT command
275
+ 0x00, // Reserved
276
+ addressType, // Address type
277
+ ]),
278
+ addressBuffer,
279
+ Buffer.from([(port >> 8) & 0xFF, port & 0xFF]), // Port (2 bytes)
280
+ ])
281
+
282
+ socket.write(request)
283
+
284
+ socket.once('data', (data) => {
285
+ if (data.length < 10 || data[0] !== 0x05) {
286
+ callback(new Error('Invalid SOCKS5 connect response'))
287
+ return
288
+ }
289
+
290
+ const status = data[1]
291
+ if (status !== 0x00) {
292
+ const errors: Record<number, string> = {
293
+ 0x01: 'General SOCKS server failure',
294
+ 0x02: 'Connection not allowed by ruleset',
295
+ 0x03: 'Network unreachable',
296
+ 0x04: 'Host unreachable',
297
+ 0x05: 'Connection refused',
298
+ 0x06: 'TTL expired',
299
+ 0x07: 'Command not supported',
300
+ 0x08: 'Address type not supported',
301
+ }
302
+ const errorMsg = status === undefined
303
+ ? 'No SOCKS5 status returned'
304
+ : (errors as any)[status] || `Unknown error (${status})`
305
+ callback(new Error(`SOCKS5 connection failed: ${errorMsg}`))
306
+ return
307
+ }
308
+
309
+ callback(null, { socket })
310
+ })
311
+ }
312
+
313
+ /**
314
+ * Check if string is a valid IPv4 address
315
+ */
316
+ private isIPv4(address: string): boolean {
317
+ const ipv4Regex = /^(?:\d{1,3}\.){3}\d{1,3}$/
318
+ if (!ipv4Regex.test(address))
319
+ return false
320
+
321
+ const parts = address.split('.').map(Number)
322
+ return parts.every(part => part >= 0 && part <= 255)
323
+ }
324
+
325
+ /**
326
+ * Check if string is a valid IPv6 address (simplified)
327
+ */
328
+ private isIPv6(address: string): boolean {
329
+ return address.includes(':') && address.split(':').length >= 3
330
+ }
331
+ }
package/src/types.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Response returned from whois function. Contains the raw text from WhoIs server and parsed/fornatted WhoIs data (if parsed is true)
3
+ *
4
+ * @interface
5
+ */
6
+ export interface WhoIsResponse {
7
+ /**
8
+ * Raw text response from WhoIs server
9
+ */
10
+ _raw: string
11
+ /**
12
+ * Parsed/Formatted key-value pairs of the response (if parsed is true)
13
+ */
14
+ parsedData: any | null
15
+ }
16
+
17
+ /**
18
+ * Type of the proxy. Either SOCKS4 or SOCKS5
19
+ * @enum
20
+ */
21
+ export enum ProxyType {
22
+ /**
23
+ * SOCKS4 type of proxy
24
+ */
25
+ SOCKS4 = 0,
26
+ /**
27
+ * SOCKS5 type of proxy
28
+ */
29
+ SOCKS5 = 1,
30
+ }
31
+
32
+ /**
33
+ * Proxy related data
34
+ * @interface
35
+ *
36
+ */
37
+ export interface ProxyData {
38
+ /**
39
+ * Proxy IP
40
+ */
41
+ ip: string
42
+ /**
43
+ * Proxy port
44
+ */
45
+ port: number
46
+ /**
47
+ * Username to connect to the proxy
48
+ */
49
+ username?: string | null
50
+ /**
51
+ * Password to connect to the proxy
52
+ */
53
+ password?: string | null
54
+ /**
55
+ * {@link ProxyType}
56
+ */
57
+ type: ProxyType
58
+ }
59
+
60
+ /**
61
+ * WhoIs options
62
+ * @interface
63
+ */
64
+ export interface WhoIsOptions {
65
+ /**
66
+ * TLD of the domain. If the {@link tld} is not provided (or null), then it will be automatically determined as to the given domain name
67
+ */
68
+ tld?: string | null
69
+ /**
70
+ * The encoding type used for WhoIs server and response. By default UTF-8 is used.
71
+ */
72
+ encoding?: string | null
73
+
74
+ /** {@link ProxyData} */
75
+ proxy?: ProxyData | null
76
+ /**
77
+ * The WhoIs server to collect data from. If not provided, the server will automatically determined using the {@link tld}
78
+ */
79
+ server?: string | null
80
+ /**
81
+ * The port of the WhoIs server. By default, port 43 is used.
82
+ */
83
+ serverPort?: number | null
84
+ /**
85
+ * Which data needs to be extracted/parsed from the WhoIs response.
86
+ * An object can be passed which contains keys of the fields of the WhoIs response.
87
+ * A copy of the provided object will be returned with the values filled for the provided keys.
88
+ *
89
+ * The keys can have default value of empty string. However, if the WhoIs response has multiple values for the same field (eg: 'Domain Status'),
90
+ * then all the values can be collected by providing a default value of an Array([]).
91
+ *
92
+ * Following example shows an object used to collect 'Domain Name', 'Domain Status' (multiple values) and 'Registrar' from WhoIs response
93
+ *
94
+ * @example {'Domain Name': '', 'Domain Status': [], 'Registrar': ''}
95
+ */
96
+ parseData?: object | null
97
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copy an Object with its values
3
+ *
4
+ * @param obj Object which needs to be copied
5
+ * @returns A copy of the object
6
+ */
7
+ export function shallowCopy<T>(obj: T): T {
8
+ if (Array.isArray(obj)) {
9
+ return obj.slice() as T // Clone the array
10
+ }
11
+
12
+ if (typeof obj === 'object' && obj !== null) {
13
+ const copy: any = {}
14
+ for (const key in obj) {
15
+ if (Object.prototype.hasOwnProperty.call(obj, key))
16
+ copy[key] = shallowCopy(obj[key])
17
+ }
18
+ return copy as T
19
+ }
20
+
21
+ return obj // For primitive values, return as is
22
+ }