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