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