build-speedtest-domainset.ts 7.8 KB

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