is-domain-alive.ts 11 KB

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