is-domain-alive.ts 11 KB

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