build-speedtest-domainset.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { domainDeduper } from './lib/domain-deduper';
  2. import path from 'path';
  3. import { createRuleset } from './lib/create-file';
  4. import { sortDomains } from './lib/stable-sort-domain';
  5. import { Sema } from 'async-sema';
  6. import { getHostname } from 'tldts';
  7. import { task } from './trace';
  8. import { fetchWithRetry } from './lib/fetch-retry';
  9. import { SHARED_DESCRIPTION } from './lib/constants';
  10. import picocolors from 'picocolors';
  11. import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
  12. import { TTL, deserializeArray, fsFetchCache, serializeArray } from './lib/cache-filesystem';
  13. import { createTrie } from './lib/trie';
  14. const s = new Sema(2);
  15. const latestTopUserAgentsPromise = fsFetchCache.apply(
  16. 'https://cdn.jsdelivr.net/npm/top-user-agents@latest/src/desktop.json',
  17. () => fetchWithRetry('https://cdn.jsdelivr.net/npm/top-user-agents@latest/src/desktop.json')
  18. .then(res => res.json() as Promise<string[]>)
  19. .then((userAgents) => userAgents.filter(ua => ua.startsWith('Mozilla/5.0 '))),
  20. {
  21. serializer: serializeArray,
  22. deserializer: deserializeArray,
  23. ttl: TTL.THREE_DAYS()
  24. }
  25. );
  26. const querySpeedtestApi = async (keyword: string): Promise<Array<string | null>> => {
  27. const topUserAgents = await latestTopUserAgentsPromise;
  28. const url = `https://www.speedtest.net/api/js/servers?engine=js&search=${keyword}&limit=100`;
  29. try {
  30. const randomUserAgent = topUserAgents[Math.floor(Math.random() * topUserAgents.length)];
  31. return await fsFetchCache.apply(
  32. url,
  33. () => s.acquire().then(() => fetchWithRetry(url, {
  34. headers: {
  35. dnt: '1',
  36. Referer: 'https://www.speedtest.net/',
  37. accept: 'application/json, text/plain, */*',
  38. 'User-Agent': randomUserAgent,
  39. 'Accept-Language': 'en-US,en;q=0.9',
  40. ...(randomUserAgent.includes('Chrome')
  41. ? {
  42. 'Sec-Ch-Ua-Mobile': '?0',
  43. 'Sec-Fetch-Dest': 'empty',
  44. 'Sec-Fetch-Mode': 'cors',
  45. 'Sec-Fetch-Site': 'same-origin',
  46. 'Sec-Gpc': '1'
  47. }
  48. : {})
  49. },
  50. signal: AbortSignal.timeout(1000 * 4),
  51. retry: {
  52. retries: 2
  53. }
  54. })).then(r => r.json() as any).then((data: Array<{ url: string }>) => data.reduce<string[]>(
  55. (prev, cur) => {
  56. const hn = getHostname(cur.url, { detectIp: false, validateHostname: true });
  57. if (hn) {
  58. prev.push(hn);
  59. }
  60. return prev;
  61. }, []
  62. )).finally(() => s.release()),
  63. {
  64. ttl: TTL.ONE_WEEK(),
  65. serializer: serializeArray,
  66. deserializer: deserializeArray
  67. }
  68. );
  69. } catch (e) {
  70. console.error(e);
  71. return [];
  72. }
  73. };
  74. export const buildSpeedtestDomainSet = task(import.meta.main, import.meta.path)(async (span) => {
  75. const domainTrie = createTrie(
  76. [
  77. // speedtest.net
  78. '.speedtest.net',
  79. '.speedtestcustom.com',
  80. '.ooklaserver.net',
  81. '.speed.misaka.one',
  82. '.speedtest.rt.ru',
  83. '.speedtest.aptg.com.tw',
  84. '.speedtest.gslnetworks.com',
  85. '.speedtest.jsinfo.net',
  86. '.speedtest.i3d.net',
  87. '.speedtestkorea.com',
  88. '.speedtest.telus.com',
  89. '.speedtest.telstra.net',
  90. '.speedtest.clouvider.net',
  91. '.speedtest.idv.tw',
  92. '.speedtest.frontier.com',
  93. '.speedtest.orange.fr',
  94. '.speedtest.centurylink.net',
  95. '.srvr.bell.ca',
  96. '.speedtest.contabo.net',
  97. 'speedtest.hk.chinamobile.com',
  98. 'speedtestbb.hk.chinamobile.com',
  99. '.hizinitestet.com',
  100. '.linknetspeedtest.net.br',
  101. 'speedtest.rit.edu',
  102. 'speedtest.ropa.de',
  103. 'speedtest.sits.su',
  104. 'speedtest.tigo.cr',
  105. 'speedtest.upp.com',
  106. '.speedtest.pni.tw',
  107. '.speed.pfm.gg',
  108. '.speedtest.faelix.net',
  109. '.speedtest.labixe.net',
  110. '.speedtest.warian.net',
  111. '.speedtest.starhub.com',
  112. '.speedtest.gibir.net.tr',
  113. '.speedtest.ozarksgo.net',
  114. '.speedtest.exetel.com.au',
  115. '.speedtest.sbcglobal.net',
  116. '.speedtest.leaptel.com.au',
  117. '.speedtest.windstream.net',
  118. '.speedtest.vodafone.com.au',
  119. '.speedtest.rascom.ru',
  120. '.speedtest.dchost.com',
  121. '.speedtest.highnet.com',
  122. '.speedtest.seattle.wa.limewave.net',
  123. '.speedtest.optitel.com.au',
  124. '.speednet.net.tr',
  125. '.speedtest.angolacables.co.ao',
  126. '.ookla-speedtest.fsr.com',
  127. '.speedtest.comnet.com.tr',
  128. '.speedtest.gslnetworks.com.au',
  129. '.test.gslnetworks.com.au',
  130. '.speedtest.gslnetworks.com',
  131. '.speedtestunonet.com.br',
  132. '.speedtest.alagas.net',
  133. 'speedtest.surfshark.com',
  134. '.speedtest.aarnet.net.au',
  135. '.ookla.rcp.net',
  136. '.ookla-speedtests.e2ro.com',
  137. '.speedtest.com.sg',
  138. '.ookla.ddnsgeek.com',
  139. '.speedtest.pni.tw',
  140. // Cloudflare
  141. '.speed.cloudflare.com',
  142. // Wi-Fi Man
  143. '.wifiman.com',
  144. '.wifiman.me',
  145. '.wifiman.ubncloud.com',
  146. // Fast.com
  147. '.fast.com',
  148. // MacPaw
  149. 'speedtest.macpaw.com',
  150. // speedtestmaster
  151. '.netspeedtestmaster.com',
  152. // Google Search Result of "speedtest", powered by this
  153. '.measurement-lab.org',
  154. '.measurementlab.net',
  155. // Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com)
  156. '.speed.googlefiber.net',
  157. // librespeed
  158. '.backend.librespeed.org',
  159. // Apple,
  160. 'mensura.cdn-apple.com', // From netQuality command
  161. // OpenSpeedtest
  162. 'open.cachefly.net'
  163. ],
  164. true,
  165. true
  166. );
  167. await span.traceChildAsync(
  168. 'fetch previous speedtest domainset',
  169. async () => {
  170. try {
  171. const contents = await readFileIntoProcessedArray(path.resolve(import.meta.dir, '../List/domainset/speedtest.conf'));
  172. contents.reduce<string[]>((acc, line) => {
  173. const hn = getHostname(line, { detectIp: false, validateHostname: true });
  174. if (hn) {
  175. acc.push(hn);
  176. }
  177. return acc;
  178. }, []).forEach(domainTrie.add);
  179. } catch { }
  180. }
  181. );
  182. await new Promise<void>((resolve, reject) => {
  183. const pMap = ([
  184. 'Hong Kong',
  185. 'Taiwan',
  186. 'China Telecom',
  187. 'China Mobile',
  188. 'China Unicom',
  189. 'Japan',
  190. 'Tokyo',
  191. 'Singapore',
  192. 'Korea',
  193. 'Seoul',
  194. 'Canada',
  195. 'Toronto',
  196. 'Montreal',
  197. 'Los Ang',
  198. 'San Jos',
  199. 'Seattle',
  200. 'New York',
  201. 'Dallas',
  202. 'Miami',
  203. 'Berlin',
  204. 'Frankfurt',
  205. 'London',
  206. 'Paris',
  207. 'Amsterdam',
  208. 'Moscow',
  209. 'Australia',
  210. 'Sydney',
  211. 'Brazil',
  212. 'Turkey'
  213. ]).reduce<Record<string, Promise<void>>>((pMap, keyword) => {
  214. pMap[keyword] = span.traceChildAsync(`fetch speedtest endpoints: ${keyword}`, () => querySpeedtestApi(keyword)).then(hostnameGroup => {
  215. return hostnameGroup.forEach(hostname => {
  216. if (hostname) {
  217. domainTrie.add(hostname);
  218. }
  219. });
  220. });
  221. return pMap;
  222. }, {});
  223. const timer = setTimeout(() => {
  224. console.error(picocolors.red('Task timeout!'));
  225. Object.entries(pMap).forEach(([name, p]) => {
  226. console.log(`[${name}]`, Bun.peek.status(p));
  227. });
  228. resolve();
  229. }, 1000 * 60 * 1.5);
  230. Promise.all(Object.values(pMap)).then(() => {
  231. clearTimeout(timer);
  232. return resolve();
  233. }).catch(() => reject);
  234. });
  235. const deduped = span.traceChildSync('sort result', () => sortDomains(domainDeduper(domainTrie)));
  236. const description = [
  237. ...SHARED_DESCRIPTION,
  238. '',
  239. 'This file contains common speedtest endpoints.'
  240. ];
  241. return createRuleset(
  242. span,
  243. 'Sukka\'s Ruleset - Speedtest Domains',
  244. description,
  245. new Date(),
  246. deduped,
  247. 'domainset',
  248. path.resolve(import.meta.dir, '../List/domainset/speedtest.conf'),
  249. path.resolve(import.meta.dir, '../Clash/domainset/speedtest.txt')
  250. );
  251. });