1 | // jshint esversion: 8, -W069 |
||
2 | /** global: conversion, general, assumptions, timeout, follow, timeBetween */ |
||
3 | |||
4 | const psl = require('psl'), |
||
5 | puny = require('punycode'), |
||
6 | uts46 = require('idna-uts46'), |
||
7 | whois = require('whois'), |
||
8 | parseRawData = require('./parseRawData'), |
||
9 | debug = require('debug')('common.whoisWrapper'), |
||
10 | { |
||
11 | getDate |
||
12 | } = require('./conversions'), |
||
13 | settings = require('./settings').load(); |
||
14 | |||
15 | |||
16 | /* |
||
17 | lookupPromise |
||
18 | Promisified whois lookup |
||
19 | */ |
||
20 | const lookupPromise = (...args) => { |
||
21 | return new Promise((resolve, reject) => { |
||
22 | whois.lookup(...args, (err, data) => { |
||
23 | if (err) return reject(err); |
||
24 | resolve(data); |
||
25 | return undefined; |
||
26 | }); |
||
27 | }); |
||
28 | }; |
||
29 | |||
30 | /* |
||
31 | lookup |
||
32 | Do a domain whois lookup |
||
33 | parameters |
||
34 | domain (string) - Domain name |
||
35 | options (object) - Lookup options object, refer to 'defaultoptions' var or 'settings.lookup.general/server' |
||
36 | */ |
||
37 | async function lookup(domain, options = getWhoisOptions()) { |
||
38 | var { |
||
39 | 'lookup.conversion': conversion, |
||
40 | 'lookup.general': general |
||
41 | } = settings, |
||
42 | domainResults; |
||
43 | |||
44 | try { |
||
45 | domain = conversion.enabled ? convertDomain(domain) : domain; |
||
46 | domain = general.psl ? psl.get(domain).replace(/((\*\.)*)/g, '') : domain; |
||
47 | |||
48 | debug("Looking up for {0}".format(domain)); |
||
49 | domainResults = await lookupPromise(domain, options); |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
50 | } catch (e) { |
||
51 | domainResults = "Whois lookup error, {0}".format(e); |
||
52 | } |
||
53 | |||
54 | return domainResults; |
||
55 | } |
||
56 | |||
57 | /* |
||
58 | toJSON |
||
59 | Transform a given string to JSON object |
||
60 | parameters |
||
61 | resultsText (string) - whois domain reply string |
||
62 | */ |
||
63 | function toJSON(resultsText) { |
||
64 | if (typeof resultsText === 'string' && resultsText.includes("lookup: timeout")) return "timeout"; |
||
65 | |||
66 | if (typeof resultsText === 'object') { |
||
67 | //JSON.stringify(resultsText, null, 2); |
||
68 | resultsText.map(function(data) { |
||
69 | data.data = parseRawData(data.data); |
||
70 | return data; |
||
71 | }); |
||
72 | } else { |
||
73 | return parseRawData(preStringStrip(resultsText)); |
||
74 | } |
||
75 | |||
76 | return undefined; |
||
77 | } |
||
78 | |||
79 | /* |
||
80 | isDomainAvailable |
||
81 | Check domain whois reply for its avalability |
||
82 | parameters |
||
83 | resultsText (string) - Pure text whois reply |
||
84 | resultsJSON (JSON Object) - JSON transformed whois reply |
||
85 | */ |
||
86 | function isDomainAvailable(resultsText, resultsJSON) { |
||
87 | const { |
||
88 | 'lookup.assumptions': assumptions |
||
89 | } = settings; |
||
90 | |||
91 | resultsJSON = resultsJSON || 0; |
||
92 | |||
93 | if (resultsJSON === 0) resultsJSON = toJSON(resultsText); |
||
94 | |||
95 | var domainParams = getDomainParameters(null, null, null, resultsJSON, true); |
||
96 | var controlDate = getDate(Date.now()); |
||
97 | |||
98 | switch (true) { |
||
99 | /* |
||
100 | Special cases |
||
101 | */ |
||
102 | case (resultsText.includes('Uniregistry') && resultsText.includes('Query limit exceeded')): |
||
103 | return (assumptions.uniregistry ? 'unavailable' : 'error:ratelimiting'); |
||
104 | |||
105 | /* |
||
106 | Available checks |
||
107 | */ |
||
108 | |||
109 | // Not found cases & variants |
||
110 | //case (resultsText.includes('ERROR:101: no entries found')): |
||
111 | |||
112 | |||
113 | // No match cases & variants |
||
114 | case (resultsText.includes('No match for domain')): |
||
115 | case (resultsText.includes('- No Match')): |
||
116 | case (resultsText.includes('NO MATCH:')): |
||
117 | case (resultsText.includes('No match for')): |
||
118 | case (resultsText.includes('No match')): |
||
119 | case (resultsText.includes('No matching record.')): |
||
120 | case (resultsText.includes('Nincs talalat')): |
||
121 | |||
122 | // Status cases & variants |
||
123 | case (resultsText.includes('Status: AVAILABLE')): |
||
124 | case (resultsText.includes('Status: AVAILABLE')): |
||
125 | case (resultsText.includes('Status: available')): |
||
126 | case (resultsText.includes('Status: free')): |
||
127 | case (resultsText.includes('Status: Not Registered')): |
||
128 | case (resultsText.includes('query_status: 220 Available')): |
||
129 | |||
130 | // Unique cases |
||
131 | case (domainParams.expiryDate - controlDate < 0): |
||
132 | case (resultsText.includes('This domain name has not been registered')): |
||
133 | case (resultsText.includes('The domain has not been registered')): |
||
134 | case (resultsText.includes('This query returned 0 objects')): |
||
135 | case (resultsText.includes(' is free') && domainParams.whoisreply.length < 50): |
||
136 | case (resultsText.includes('domain name not known in')): |
||
137 | case (resultsText.includes('registration status: available')): |
||
138 | case (resultsText.includes('whois.nic.bo') && domainParams.whoisreply.length < 55): |
||
139 | case (resultsText.includes('Object does not exist')): |
||
140 | case (resultsText.includes('The queried object does not exist')): |
||
141 | case (resultsText.includes('Not Registered -')): |
||
142 | case (resultsText.includes('is available for registration')): |
||
143 | case (resultsText.includes('is available for purchase')): |
||
144 | case (resultsText.includes('DOMAIN IS NOT A REGISTERD')): |
||
145 | case (resultsText.includes('No such domain')): |
||
146 | case (resultsText.includes('No_Se_Encontro_El_Objeto')): |
||
147 | case (resultsText.includes('Domain unknown')): |
||
148 | case (resultsText.includes('No information available about domain name')): |
||
149 | case (resultsText.includes('Error.') && resultsText.includes('SaudiNIC')): |
||
150 | case (resultsText.includes('is not valid!')): // ??? |
||
151 | return 'available'; |
||
152 | |||
153 | /* |
||
154 | Unavailable checks |
||
155 | */ |
||
156 | case (resultsJSON.hasOwnProperty('domainName')): // Has domain name |
||
157 | case (resultsText.includes('Domain Status:ok')): // Domain name is ok |
||
158 | case (resultsText.includes('Expiration Date:')): // Has expiration date (1) |
||
159 | case (resultsText.includes('Expiry Date:')): // Has Expiration date (2) |
||
160 | case (resultsText.includes('Status: connect')): // Has connect status |
||
161 | case (resultsText.includes('Changed:')): // Has a changed date |
||
162 | case (Object.keys(resultsJSON).length > 5): // JSON has more than 5 keys (probably taken?) |
||
163 | case (resultsText.includes('organisation: Internet Assigned Numbers Authority')): // Is controlled by IANA |
||
164 | return 'unavailable'; |
||
165 | |||
166 | /* |
||
167 | Error checks |
||
168 | */ |
||
169 | |||
170 | // Error, null or no contents |
||
171 | case (resultsText === null): |
||
172 | case (resultsText === ''): |
||
173 | return 'error:nocontent'; |
||
174 | |||
175 | // Error, unauthorized |
||
176 | case (resultsText.includes('You are not authorized to access or query our Whois')): |
||
177 | return 'error:unauthorized'; |
||
178 | |||
179 | // Error, rate limiting |
||
180 | case (resultsText.includes('IP Address Has Reached Rate Limit')): |
||
181 | case (resultsText.includes('Too many connection attempts')): |
||
182 | case (resultsText.includes('Your request is being rate limited')): |
||
183 | case (resultsText.includes('Your query is too often.')): |
||
184 | case (resultsText.includes('Your connection limit exceeded.')): |
||
185 | return (assumptions.ratelimit ? 'unavailable' : 'error:ratelimiting'); |
||
186 | |||
187 | // Error, unretrivable |
||
188 | case (resultsText.includes('Could not retrieve Whois data')): |
||
189 | return 'error:unretrivable'; |
||
190 | |||
191 | // Error, forbidden |
||
192 | case (resultsText.includes('si is forbidden')): // .si is forbidden |
||
193 | case (resultsText.includes('Requests of this client are not permitted')): // .ch forbidden |
||
194 | return 'error:forbidden'; |
||
195 | |||
196 | // Error, reserved by regulator |
||
197 | case (resultsText.includes('reserved by aeDA Regulator')): // Reserved for aeDA regulator |
||
198 | return 'error:reservedbyregulator'; |
||
199 | |||
200 | // Error, unregistrable. |
||
201 | case (resultsText.includes('third-level domains may not start with')): |
||
202 | return 'error:unregistrable'; |
||
203 | |||
204 | // Error, reply error |
||
205 | case (resultsJSON.hasOwnProperty('error')): |
||
206 | case (resultsJSON.hasOwnProperty('errno')): |
||
207 | case (resultsText.includes('error ')): |
||
208 | case (resultsText.includes('error')): // includes plain error, may cause false negatives? i.e. error.com lookup |
||
209 | case (resultsText.includes('Error')): // includes plain error, may cause false negatives? i.e. error.com lookup |
||
210 | case (resultsText.includes('ERROR:101:')): |
||
211 | case (resultsText.includes('Whois lookup error')): |
||
212 | case (resultsText.includes('can temporarily not be answered')): |
||
213 | case (resultsText.includes('Invalid input')): |
||
214 | return 'error:replyerror'; |
||
215 | |||
216 | /* |
||
217 | Error throw |
||
218 | If every check fails throw Error, unparsable |
||
219 | */ |
||
220 | |||
221 | default: |
||
222 | return (assumptions.unparsable ? 'available' : 'error:unparsable'); |
||
223 | } |
||
224 | } |
||
225 | |||
226 | /* |
||
227 | getDomainParameters |
||
228 | Get streamlined domain results object |
||
229 | parameters |
||
230 | domain (string) - Domain name |
||
231 | status (string) - isDomainAvailable result, is domain Available |
||
232 | resultsText (string) - Pure text whois reply |
||
233 | resultsJSON (JSON Object) - JSON transformed whois reply |
||
234 | isAuxiliary (boolean) - Is auxiliary function to domain availability check, if used in "isDomainAvailable" fn |
||
235 | */ |
||
236 | function getDomainParameters(domain, status, resultsText, resultsJSON, isAuxiliary = false) { |
||
237 | var results = {}; |
||
238 | |||
239 | results.domain = domain; |
||
240 | results.status = status; |
||
241 | results.registrar = resultsJSON.registrar; |
||
242 | results.company = |
||
243 | resultsJSON.registrantOrganization || |
||
244 | resultsJSON.registrant || |
||
245 | resultsJSON.registrantOrganization || |
||
246 | resultsJSON.adminName || |
||
247 | resultsJSON.ownerName || |
||
248 | resultsJSON.contact || |
||
249 | resultsJSON.name; |
||
250 | results.creationDate = getDate( |
||
251 | resultsJSON.creationDate || |
||
252 | resultsJSON.createdDate || |
||
253 | resultsJSON.created || |
||
254 | resultsJSON.creationDate || |
||
255 | resultsJSON.registered || |
||
256 | resultsJSON.registeredOn); |
||
257 | results.updateDate = getDate( |
||
258 | resultsJSON.updatedDate || |
||
259 | resultsJSON.lastUpdated || |
||
260 | resultsJSON.UpdatedDate || |
||
261 | resultsJSON.changed || |
||
262 | resultsJSON.lastModified || |
||
263 | resultsJSON.lastUpdate); |
||
264 | results.expiryDate = getDate( |
||
265 | resultsJSON.expires || |
||
266 | resultsJSON.registryExpiryDate || |
||
267 | resultsJSON.expiryDate || |
||
268 | resultsJSON.registrarRegistrationExpirationDate || |
||
269 | resultsJSON.expire || |
||
270 | resultsJSON.expirationDate || |
||
271 | resultsJSON.expiresOn || |
||
272 | resultsJSON.paidTill); |
||
273 | results.whoisReply = resultsText; |
||
274 | results.whoisJson = resultsJSON; |
||
275 | |||
276 | //debug(results); |
||
277 | |||
278 | return results; |
||
279 | } |
||
280 | |||
281 | /* |
||
282 | convertDomain |
||
283 | Convert a given domain using a defined algorithm in appSettings |
||
284 | parameters |
||
285 | domain (string) - Domain to be converted |
||
286 | modes |
||
287 | punycode - Punycode |
||
288 | uts46 - IDNA2008 |
||
289 | uts46-transitional - IDNA2003 |
||
290 | ascii - Filter out non-ASCII characters |
||
291 | anything else - No conversion |
||
292 | */ |
||
293 | function convertDomain(domain, mode) { |
||
294 | var { |
||
295 | 'lookup.conversion': conversion |
||
296 | } = settings; |
||
297 | |||
298 | mode = mode || conversion.algorithm; |
||
299 | |||
300 | switch (mode) { |
||
301 | case 'punycode': |
||
302 | return puny.encode(domain); |
||
303 | case 'uts46': |
||
304 | return uts46.toAscii(domain); |
||
305 | case 'uts46-transitional': |
||
306 | return uts46.toAscii(domain, { |
||
307 | transitional: true |
||
308 | }); |
||
309 | case 'ascii': |
||
310 | return domain.replace(/[^\x00-\x7F]/g, ""); |
||
311 | |||
312 | default: |
||
313 | return domain; |
||
314 | } |
||
315 | } |
||
316 | |||
317 | /* |
||
318 | getWhoisOptions |
||
319 | Create whois options based on appSettings |
||
320 | */ |
||
321 | function getWhoisOptions() { |
||
322 | const { |
||
323 | 'lookup.general': general |
||
324 | } = settings; |
||
325 | |||
326 | var options = {}, |
||
327 | follow = 'follow', |
||
328 | timeout = 'timeout'; |
||
329 | |||
330 | options.server = general.server; |
||
331 | options.follow = getWhoisParameters(follow); |
||
332 | options.timeout = getWhoisParameters(timeout); |
||
333 | options.verbose = general.verbose; |
||
334 | |||
335 | return options; |
||
336 | } |
||
337 | |||
338 | /* |
||
339 | getWhoisParameters |
||
340 | Get request follow level/depth |
||
341 | parameters |
||
342 | parameter (string) - Whois options parameter |
||
343 | 'follow' - Follow depth |
||
344 | 'timeout' - Timeout |
||
345 | 'timebetween' - Time between requests |
||
346 | */ |
||
347 | function getWhoisParameters(parameter) { |
||
348 | const { |
||
349 | 'lookup.randomize.follow': follow, |
||
350 | 'lookup.randomize.timeout': timeout, |
||
351 | 'lookup.randomize.timeBetween': timeBetween, |
||
352 | 'lookup.general': general |
||
353 | } = settings; |
||
354 | |||
355 | switch (parameter) { |
||
356 | case 'follow': |
||
357 | debug("Follow depth, 'random': {0}, 'maximum': {1}, 'minimum': {2}, 'default': {3}".format(follow.randomize, follow.maximumDepth, follow.minimumDepth, general.follow)); |
||
358 | return (follow.randomize ? getRandomInt(follow.minimumDepth, follow.maximumDepth) : general.follow); |
||
359 | |||
360 | case 'timeout': |
||
361 | debug("Timeout, 'random': {0}, 'maximum': {1}, 'minimum': {2}, 'default': {3}".format(timeout.randomize, timeout.maximum, timeout.minimum, general.timeout)); |
||
362 | return (timeout.randomize ? getRandomInt(timeout.minimum, timeout.maximum) : general.timeout); |
||
363 | |||
364 | case 'timebetween': |
||
365 | debug("Timebetween, 'random': {0}, 'maximum': {1}, 'minimum': {2}, 'default': {3}".format(timeBetween.randomize, timeBetween.maximum, timeBetween.minimum, general.timeBetween)); |
||
366 | return (timeBetween.randomize ? getRandomInt(timeBetween.minimum, timeBetween.maximum) : general.timeBetween); |
||
367 | |||
368 | default: |
||
369 | return undefined; |
||
370 | |||
371 | } |
||
372 | |||
373 | } |
||
374 | |||
375 | /* |
||
376 | getRandomInt |
||
377 | Get a random integer between two values |
||
378 | parameters |
||
379 | min (integer) - Minimum value |
||
380 | max (integer) - Maximum value |
||
381 | */ |
||
382 | function getRandomInt(min, max) { |
||
383 | return Math.floor((Math.random() * parseInt(max)) + parseInt(min)); |
||
384 | } |
||
385 | |||
386 | /* |
||
387 | preStringStrip |
||
388 | Pre strip a given string, space key value pairs |
||
389 | parameters |
||
390 | str (string) - String to be stripped |
||
391 | */ |
||
392 | function preStringStrip(str) { |
||
393 | return str.toString().replace(/\:\t{1,2}/g, ": "); // Space key value pairs |
||
394 | } |
||
395 | |||
396 | module.exports = { |
||
397 | lookup: lookup, |
||
398 | toJSON: toJSON, |
||
399 | isDomainAvailable: isDomainAvailable, |
||
400 | preStringStrip: preStringStrip, |
||
401 | getDomainParameters: getDomainParameters, |
||
402 | convertDomain: convertDomain |
||
403 | }; |
||
404 |