is-domain-alive.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import tldts from 'tldts-experimental';
  2. import { looseTldtsOpt } from '../constants/loose-tldts-opt';
  3. import picocolors from 'picocolors';
  4. import { pickRandom, pickOne } from 'foxts/pick-random';
  5. import DNS2 from 'dns2';
  6. import asyncRetry from 'async-retry';
  7. import * as whoiser from 'whoiser';
  8. import { createRetrieKeywordFilter as createKeywordFilter } from 'foxts/retrie';
  9. import process from 'node:process';
  10. const mutex = new Map<string, Promise<unknown>>();
  11. export function keyedAsyncMutexWithQueue<T>(key: string, fn: () => Promise<T>) {
  12. if (mutex.has(key)) {
  13. return mutex.get(key) as Promise<T>;
  14. }
  15. const promise = fn();
  16. mutex.set(key, promise);
  17. return promise;
  18. }
  19. class DnsError extends Error {
  20. name = 'DnsError';
  21. constructor(readonly message: string, public readonly server: string) {
  22. super(message);
  23. }
  24. }
  25. interface DnsResponse extends DNS2.$DnsResponse {
  26. dns: string
  27. }
  28. const dohServers: Array<[string, DNS2.DnsResolver]> = ([
  29. '8.8.8.8',
  30. '8.8.4.4',
  31. '1.0.0.1',
  32. '1.1.1.1',
  33. '162.159.36.1',
  34. '162.159.46.1',
  35. 'dns.cloudflare.com', // Cloudflare DoH that uses different IPs
  36. // one.one.one.one // Cloudflare DoH that uses 1.1.1.1 and 1.0.0.1
  37. '101.101.101.101', // TWNIC
  38. '185.222.222.222', // DNS.SB
  39. '45.11.45.11', // DNS.SB
  40. 'doh.dns.sb', // DNS.SB, Different PoPs w/ GeoDNS
  41. // 'doh.sb', // DNS.SB xTom Anycast IP
  42. // 'dns.sb', // DNS.SB use same xTom Anycast IP as doh.sb
  43. 'dns10.quad9.net', // Quad9 unfiltered
  44. 'doh.sandbox.opendns.com', // OpenDNS sandbox (unfiltered)
  45. 'unfiltered.adguard-dns.com',
  46. // '0ms.dev', // Proxy Cloudflare
  47. // '76.76.2.0', // ControlD unfiltered, path not /dns-query
  48. // '76.76.10.0', // ControlD unfiltered, path not /dns-query
  49. // 'dns.bebasid.com', // BebasID, path not /dns-query but /unfiltered
  50. // '193.110.81.0', // dns0.eu
  51. // '185.253.5.0', // dns0.eu
  52. // 'zero.dns0.eu',
  53. 'dns.nextdns.io',
  54. 'anycast.dns.nextdns.io',
  55. 'wikimedia-dns.org',
  56. 'puredns.org',
  57. // 'ordns.he.net',
  58. // 'dns.mullvad.net',
  59. 'basic.rethinkdns.com'
  60. // '198.54.117.10' // NameCheap DNS, supports DoT, DoH, UDP53
  61. // 'ada.openbld.net',
  62. // 'dns.rabbitdns.org'
  63. ] as const).map(dns => [
  64. dns,
  65. DNS2.DOHClient({
  66. dns,
  67. http: false
  68. })
  69. ] as const);
  70. const domesticDohServers: Array<[string, DNS2.DnsResolver]> = ([
  71. '223.5.5.5',
  72. '223.6.6.6',
  73. '120.53.53.53',
  74. '1.12.12.12'
  75. ] as const).map(dns => [
  76. dns,
  77. DNS2.DOHClient({
  78. dns,
  79. http: false
  80. })
  81. ] as const);
  82. async function $resolve(name: string, type: DNS2.PacketQuestion, server: [string, DNS2.DnsResolver]) {
  83. try {
  84. return await asyncRetry(async () => {
  85. const [dohServer, dohClient] = server;
  86. try {
  87. return {
  88. ...await dohClient(name, type),
  89. dns: dohServer
  90. } satisfies DnsResponse;
  91. } catch (e) {
  92. // console.error(e);
  93. throw new DnsError((e as Error).message, dohServer);
  94. }
  95. }, { retries: 5 });
  96. } catch (e) {
  97. console.log('[doh error]', name, type, e);
  98. throw e;
  99. }
  100. }
  101. async function getWhois(domain: string) {
  102. return asyncRetry(() => whoiser.domain(domain, { raw: true }), { retries: 5 });
  103. }
  104. const domainAliveMap = new Map<string, boolean>();
  105. function onDomainAlive(domain: string): [string, boolean] {
  106. domainAliveMap.set(domain, true);
  107. return [domain, true];
  108. }
  109. function onDomainDead(domain: string): [string, boolean] {
  110. domainAliveMap.set(domain, false);
  111. return [domain, false];
  112. }
  113. export async function isDomainAlive(domain: string, isSuffix: boolean): Promise<[string, boolean]> {
  114. if (domainAliveMap.has(domain)) {
  115. return [domain, domainAliveMap.get(domain)!];
  116. }
  117. const apexDomain = tldts.getDomain(domain, looseTldtsOpt);
  118. if (!apexDomain) {
  119. console.log(picocolors.gray('[domain invalid]'), picocolors.gray('no apex domain'), { domain });
  120. return onDomainAlive(domain);
  121. }
  122. const apexDomainAlive = await keyedAsyncMutexWithQueue(apexDomain, () => isApexDomainAlive(apexDomain));
  123. if (isSuffix) {
  124. return apexDomainAlive;
  125. }
  126. if (!apexDomainAlive[1]) {
  127. return apexDomainAlive;
  128. }
  129. const $domain = domain[0] === '.' ? domain.slice(1) : domain;
  130. const aDns: string[] = [];
  131. const aaaaDns: string[] = [];
  132. // test 2 times before make sure record is empty
  133. const servers = pickRandom(dohServers, 2);
  134. for (let i = 0; i < 2; i++) {
  135. // eslint-disable-next-line no-await-in-loop -- sequential
  136. const aRecords = (await $resolve($domain, 'A', servers[i]));
  137. if (aRecords.answers.length > 0) {
  138. return onDomainAlive(domain);
  139. }
  140. aDns.push(aRecords.dns);
  141. }
  142. for (let i = 0; i < 2; i++) {
  143. // eslint-disable-next-line no-await-in-loop -- sequential
  144. const aaaaRecords = (await $resolve($domain, 'AAAA', servers[i]));
  145. if (aaaaRecords.answers.length > 0) {
  146. return onDomainAlive(domain);
  147. }
  148. aaaaDns.push(aaaaRecords.dns);
  149. }
  150. // only then, let's test twice with domesticDohServers
  151. for (let i = 0; i < 2; i++) {
  152. // eslint-disable-next-line no-await-in-loop -- sequential
  153. const aRecords = (await $resolve($domain, 'A', pickOne(domesticDohServers)));
  154. if (aRecords.answers.length > 0) {
  155. return onDomainAlive(domain);
  156. }
  157. aDns.push(aRecords.dns);
  158. }
  159. for (let i = 0; i < 2; i++) {
  160. // eslint-disable-next-line no-await-in-loop -- sequential
  161. const aaaaRecords = (await $resolve($domain, 'AAAA', pickOne(domesticDohServers)));
  162. if (aaaaRecords.answers.length > 0) {
  163. return onDomainAlive(domain);
  164. }
  165. aaaaDns.push(aaaaRecords.dns);
  166. }
  167. console.log(picocolors.red('[domain dead]'), 'no A/AAAA records', { domain, a: aDns, aaaa: aaaaDns });
  168. return onDomainDead($domain);
  169. }
  170. const apexDomainNsResolvePromiseMap = new Map<string, Promise<boolean>>();
  171. async function getNS(domain: string) {
  172. const servers = pickRandom(dohServers, 2);
  173. for (let i = 0, len = servers.length; i < len; i++) {
  174. const server = servers[i];
  175. // eslint-disable-next-line no-await-in-loop -- one by one
  176. const resp = await $resolve(domain, 'NS', server);
  177. if (resp.answers.length > 0) {
  178. return true;
  179. }
  180. }
  181. return false;
  182. }
  183. async function isApexDomainAlive(apexDomain: string): Promise<[string, boolean]> {
  184. if (domainAliveMap.has(apexDomain)) {
  185. return [apexDomain, domainAliveMap.get(apexDomain)!];
  186. }
  187. let hasNS: boolean;
  188. if (apexDomainNsResolvePromiseMap.has(apexDomain)) {
  189. hasNS = await apexDomainNsResolvePromiseMap.get(apexDomain)!;
  190. } else {
  191. const promise = getNS(apexDomain);
  192. apexDomainNsResolvePromiseMap.set(apexDomain, promise);
  193. hasNS = await promise;
  194. }
  195. if (hasNS) {
  196. return onDomainAlive(apexDomain);
  197. }
  198. let whois;
  199. try {
  200. whois = await getWhois(apexDomain);
  201. } catch (e) {
  202. console.log(picocolors.red('[whois error]'), { domain: apexDomain }, e);
  203. return onDomainAlive(apexDomain);
  204. }
  205. if (process.env.DEBUG) {
  206. console.log(JSON.stringify(whois, null, 2));
  207. }
  208. const whoisError = noWhois(whois);
  209. if (!whoisError) {
  210. console.log(picocolors.gray('[domain alive]'), picocolors.gray('whois found'), { domain: apexDomain });
  211. return onDomainAlive(apexDomain);
  212. }
  213. console.log(picocolors.red('[domain dead]'), 'whois not found', { domain: apexDomain, err: whoisError });
  214. return onDomainDead('.' + apexDomain);
  215. }
  216. // TODO: this is a workaround for https://github.com/LayeredStudio/whoiser/issues/117
  217. const whoisNotFoundKeywordTest = createKeywordFilter([
  218. 'no match for',
  219. 'does not exist',
  220. 'not found',
  221. 'no found',
  222. 'no entries',
  223. 'no data found',
  224. 'is available for registration',
  225. 'currently available for application',
  226. 'no matching record',
  227. 'no information available about domain name',
  228. 'not been registered',
  229. 'no match!!',
  230. 'status: available',
  231. ' is free',
  232. 'no object found',
  233. 'nothing found',
  234. 'status: free',
  235. // 'pendingdelete',
  236. ' has been blocked by '
  237. ]);
  238. // whois server can redirect, so whoiser might/will get info from multiple whois servers
  239. // some servers (like TLD whois servers) might have cached/outdated results
  240. // we can only make sure a domain is alive once all response from all whois servers demonstrate so
  241. export function noWhois(whois: whoiser.WhoisSearchResult): null | string {
  242. let empty = true;
  243. for (const key in whois) {
  244. if (Object.hasOwn(whois, key)) {
  245. empty = false;
  246. // if (key === 'error') {
  247. // // if (
  248. // // (typeof whois.error === 'string' && whois.error)
  249. // // || (Array.isArray(whois.error) && whois.error.length > 0)
  250. // // ) {
  251. // // console.error(whois);
  252. // // return true;
  253. // // }
  254. // continue;
  255. // }
  256. // if (key === 'text') {
  257. // if (Array.isArray(whois.text)) {
  258. // for (const value of whois.text) {
  259. // if (whoisNotFoundKeywordTest(value.toLowerCase())) {
  260. // return value;
  261. // }
  262. // }
  263. // }
  264. // continue;
  265. // }
  266. // if (key === 'Name Server') {
  267. // // if (Array.isArray(whois[key]) && whois[key].length === 0) {
  268. // // return false;
  269. // // }
  270. // continue;
  271. // }
  272. // if (key === 'Domain Status') {
  273. // if (Array.isArray(whois[key])) {
  274. // for (const status of whois[key]) {
  275. // if (status === 'free' || status === 'AVAILABLE') {
  276. // return key + ': ' + status;
  277. // }
  278. // if (whoisNotFoundKeywordTest(status.toLowerCase())) {
  279. // return key + ': ' + status;
  280. // }
  281. // }
  282. // }
  283. // continue;
  284. // }
  285. // if (typeof whois[key] === 'string' && whois[key]) {
  286. // if (whoisNotFoundKeywordTest(whois[key].toLowerCase())) {
  287. // return key + ': ' + whois[key];
  288. // }
  289. // continue;
  290. // }
  291. if (key === '__raw' && typeof whois.__raw === 'string') {
  292. const lines = whois.__raw.trim().toLowerCase().replaceAll(/[\t ]+/g, ' ').split(/\r?\n/);
  293. if (process.env.DEBUG) {
  294. console.log({ lines });
  295. }
  296. for (const line of lines) {
  297. if (whoisNotFoundKeywordTest(line)) {
  298. return line;
  299. }
  300. }
  301. continue;
  302. }
  303. if (typeof whois[key] === 'object' && !Array.isArray(whois[key])) {
  304. const tmp = noWhois(whois[key]);
  305. if (tmp) {
  306. return tmp;
  307. }
  308. continue;
  309. }
  310. }
  311. }
  312. if (empty) {
  313. return 'whois is empty';
  314. }
  315. return null;
  316. }