is-domain-alive.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 { pickRandom, pickOne } from 'foxts/pick-random';
  7. import tldts from 'tldts-experimental';
  8. import * as whoiser from 'whoiser';
  9. import process from 'node:process';
  10. import { createRetrieKeywordFilter as createKeywordFilter } from 'foxts/retrie';
  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 = pickRandom(dohServers, 2);
  103. for (let i = 0; i < 2; i++) {
  104. // eslint-disable-next-line no-await-in-loop -- sequential
  105. const aRecords = (await $resolve(domain, 'A', servers[i]));
  106. if (aRecords.answers.length > 0) {
  107. domainAliveMap.set(domain, true);
  108. return true;
  109. }
  110. aDns.push(aRecords.dns);
  111. }
  112. for (let i = 0; i < 2; i++) {
  113. // eslint-disable-next-line no-await-in-loop -- sequential
  114. const aaaaRecords = (await $resolve(domain, 'AAAA', servers[i]));
  115. if (aaaaRecords.answers.length > 0) {
  116. domainAliveMap.set(domain, true);
  117. return true;
  118. }
  119. aaaaDns.push(aaaaRecords.dns);
  120. }
  121. // only then, let's test twice with domesticDohServers
  122. for (let i = 0; i < 2; i++) {
  123. // eslint-disable-next-line no-await-in-loop -- sequential
  124. const aRecords = (await $resolve(domain, 'A', pickOne(domesticDohServers)));
  125. if (aRecords.answers.length > 0) {
  126. domainAliveMap.set(domain, true);
  127. return true;
  128. }
  129. aDns.push(aRecords.dns);
  130. }
  131. for (let i = 0; i < 2; i++) {
  132. // eslint-disable-next-line no-await-in-loop -- sequential
  133. const aaaaRecords = (await $resolve(domain, 'AAAA', pickOne(domesticDohServers)));
  134. if (aaaaRecords.answers.length > 0) {
  135. domainAliveMap.set(domain, true);
  136. return true;
  137. }
  138. aaaaDns.push(aaaaRecords.dns);
  139. }
  140. console.log(picocolors.red('[domain dead]'), 'no A/AAAA records', { domain, a: aDns, aaaa: aaaaDns });
  141. domainAliveMap.set(domain, false);
  142. return false;
  143. });
  144. }
  145. const apexDomainMap = createKeyedAsyncMutex('isApexDomainAlive');
  146. function isApexDomainAlive(apexDomain: string) {
  147. if (domainAliveMap.has(apexDomain)) {
  148. return domainAliveMap.get(apexDomain)!;
  149. }
  150. return apexDomainMap.acquire(apexDomain, async () => {
  151. const servers = pickRandom(dohServers, 2);
  152. for (let i = 0, len = servers.length; i < len; i++) {
  153. const server = servers[i];
  154. // eslint-disable-next-line no-await-in-loop -- one by one
  155. const resp = await $resolve(apexDomain, 'NS', server);
  156. if (resp.answers.length > 0) {
  157. domainAliveMap.set(apexDomain, true);
  158. return true;
  159. }
  160. }
  161. let whois;
  162. try {
  163. whois = await getWhois(apexDomain);
  164. } catch (e) {
  165. console.log(picocolors.red('[whois error]'), { domain: apexDomain }, e);
  166. domainAliveMap.set(apexDomain, true);
  167. return true;
  168. }
  169. const whoisError = noWhois(whois);
  170. if (!whoisError) {
  171. console.log(picocolors.gray('[domain alive]'), picocolors.gray('whois found'), { domain: apexDomain });
  172. domainAliveMap.set(apexDomain, true);
  173. return true;
  174. }
  175. console.log(picocolors.red('[domain dead]'), 'whois not found', { domain: apexDomain, err: whoisError });
  176. domainAliveMap.set(apexDomain, false);
  177. return false;
  178. });
  179. }
  180. async function $resolve(name: string, type: DNS2.PacketQuestion, server: [string, DNS2.DnsResolver]) {
  181. try {
  182. return await asyncRetry(async () => {
  183. const [dohServer, dohClient] = server;
  184. try {
  185. return {
  186. ...await dohClient(name, type),
  187. dns: dohServer
  188. } satisfies DnsResponse;
  189. } catch (e) {
  190. // console.error(e);
  191. throw new DnsError((e as Error).message, dohServer);
  192. }
  193. }, { retries: 5 });
  194. } catch (e) {
  195. console.log('[doh error]', name, type, e);
  196. throw e;
  197. }
  198. }
  199. async function getWhois(domain: string) {
  200. return asyncRetry(() => whoiser.domain(domain, { raw: true }), { retries: 5 });
  201. }
  202. // TODO: this is a workaround for https://github.com/LayeredStudio/whoiser/issues/117
  203. const whoisNotFoundKeywordTest = createKeywordFilter([
  204. 'no match for',
  205. 'does not exist',
  206. 'not found',
  207. 'no found',
  208. 'no entries',
  209. 'no data found',
  210. 'is available for registration',
  211. 'currently available for application',
  212. 'no matching record',
  213. 'no information available about domain name',
  214. 'not been registered',
  215. 'no match!!',
  216. 'status: available',
  217. ' is free',
  218. 'no object found',
  219. 'nothing found',
  220. 'status: free',
  221. // 'pendingdelete',
  222. ' has been blocked by '
  223. ]);
  224. // whois server can redirect, so whoiser might/will get info from multiple whois servers
  225. // some servers (like TLD whois servers) might have cached/outdated results
  226. // we can only make sure a domain is alive once all response from all whois servers demonstrate so
  227. function noWhois(whois: whoiser.WhoisSearchResult): null | string {
  228. let empty = true;
  229. for (const key in whois) {
  230. if (Object.hasOwn(whois, key)) {
  231. empty = false;
  232. // if (key === 'error') {
  233. // // if (
  234. // // (typeof whois.error === 'string' && whois.error)
  235. // // || (Array.isArray(whois.error) && whois.error.length > 0)
  236. // // ) {
  237. // // console.error(whois);
  238. // // return true;
  239. // // }
  240. // continue;
  241. // }
  242. // if (key === 'text') {
  243. // if (Array.isArray(whois.text)) {
  244. // for (const value of whois.text) {
  245. // if (whoisNotFoundKeywordTest(value.toLowerCase())) {
  246. // return value;
  247. // }
  248. // }
  249. // }
  250. // continue;
  251. // }
  252. // if (key === 'Name Server') {
  253. // // if (Array.isArray(whois[key]) && whois[key].length === 0) {
  254. // // return false;
  255. // // }
  256. // continue;
  257. // }
  258. // if (key === 'Domain Status') {
  259. // if (Array.isArray(whois[key])) {
  260. // for (const status of whois[key]) {
  261. // if (status === 'free' || status === 'AVAILABLE') {
  262. // return key + ': ' + status;
  263. // }
  264. // if (whoisNotFoundKeywordTest(status.toLowerCase())) {
  265. // return key + ': ' + status;
  266. // }
  267. // }
  268. // }
  269. // continue;
  270. // }
  271. // if (typeof whois[key] === 'string' && whois[key]) {
  272. // if (whoisNotFoundKeywordTest(whois[key].toLowerCase())) {
  273. // return key + ': ' + whois[key];
  274. // }
  275. // continue;
  276. // }
  277. if (key === '__raw' && typeof whois.__raw === 'string') {
  278. const lines = whois.__raw.trim().toLowerCase().replaceAll(/[\t ]+/g, ' ').split(/\r?\n/);
  279. if (process.env.DEBUG) {
  280. console.log({ lines });
  281. }
  282. for (const line of lines) {
  283. if (whoisNotFoundKeywordTest(line)) {
  284. return line;
  285. }
  286. }
  287. continue;
  288. }
  289. if (typeof whois[key] === 'object' && !Array.isArray(whois[key])) {
  290. const tmp = noWhois(whois[key]);
  291. if (tmp) {
  292. return tmp;
  293. }
  294. continue;
  295. }
  296. }
  297. }
  298. if (empty) {
  299. return 'whois is empty';
  300. }
  301. return null;
  302. }