build-speedtest-domainset.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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, fsCache, serializeArray } from './lib/cache-filesystem';
  15. const s = new Sema(2);
  16. const latestTopUserAgentsPromise = fsCache.apply(
  17. 'https://unpkg.com/top-user-agents@latest/src/desktop.json',
  18. () => fetchWithRetry('https://unpkg.com/top-user-agents@latest/src/desktop.json')
  19. .then(res => res.json<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 fsCache.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<Array<{ url: string }>>()).then(data => data.reduce<string[]>(
  56. (prev, cur) => {
  57. const hn = tldts.getHostname(cur.url, { detectIp: false });
  58. if (hn) {
  59. prev.push(hn);
  60. }
  61. return prev;
  62. }, []
  63. )).finally(() => s.release()),
  64. {
  65. ttl: TTL.ONE_WEEK(),
  66. serializer: serializeArray,
  67. deserializer: deserializeArray
  68. }
  69. );
  70. } catch (e) {
  71. console.error(e);
  72. return [];
  73. }
  74. };
  75. export const buildSpeedtestDomainSet = task(import.meta.path, async (span) => {
  76. // Predefined domainset
  77. /** @type {Set<string>} */
  78. const domains = new Set<string>([
  79. '.speedtest.net',
  80. '.speedtestcustom.com',
  81. '.ooklaserver.net',
  82. '.speed.misaka.one',
  83. '.speed.cloudflare.com',
  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. // Wi-Fi Man
  129. '.wifiman.com',
  130. '.wifiman.me',
  131. // Fast.com
  132. '.fast.com',
  133. // MacPaw
  134. 'speedtest.macpaw.com',
  135. // speedtestmaster
  136. '.netspeedtestmaster.com',
  137. // Google Search Result of "speedtest", powered by this
  138. '.measurement-lab.org',
  139. '.measurementlab.net',
  140. // Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com)
  141. '.speed.googlefiber.net',
  142. // librespeed
  143. '.backend.librespeed.org'
  144. ]);
  145. // Download previous speedtest domainset
  146. for await (const l of await fetchRemoteTextByLine('https://ruleset.skk.moe/List/domainset/speedtest.conf')) {
  147. const line = processLine(l);
  148. if (line) {
  149. domains.add(line);
  150. }
  151. }
  152. await new Promise<void>((resolve) => {
  153. const pMap = ([
  154. 'Hong Kong',
  155. 'Taiwan',
  156. 'China Telecom',
  157. 'China Mobile',
  158. 'China Unicom',
  159. 'Japan',
  160. 'Tokyo',
  161. 'Singapore',
  162. 'Korea',
  163. 'Canada',
  164. 'Toronto',
  165. 'Montreal',
  166. 'Los Ang',
  167. 'San Jos',
  168. 'Seattle',
  169. 'New York',
  170. 'Dallas',
  171. 'Miami',
  172. 'Berlin',
  173. 'Frankfurt',
  174. 'London',
  175. 'Paris',
  176. 'Amsterdam',
  177. 'Moscow',
  178. 'Australia',
  179. 'Sydney',
  180. 'Brazil',
  181. 'Turkey'
  182. ]).reduce<Record<string, Promise<void>>>((pMap, keyword) => {
  183. pMap[keyword] = span.traceChild(`fetch speedtest endpoints: ${keyword}`).traceAsyncFn(() => querySpeedtestApi(keyword)).then(hostnameGroup => {
  184. hostnameGroup.forEach(hostname => {
  185. if (hostname) {
  186. domains.add(hostname);
  187. }
  188. });
  189. });
  190. return pMap;
  191. }, {});
  192. const timer = setTimeout(() => {
  193. console.error(picocolors.red('Task timeout!'));
  194. Object.entries(pMap).forEach(([name, p]) => {
  195. console.log(`[${name}]`, Bun.peek.status(p));
  196. });
  197. resolve();
  198. }, 1000 * 60 * 2);
  199. Promise.all(Object.values(pMap)).then(() => {
  200. clearTimeout(timer);
  201. resolve();
  202. });
  203. });
  204. const gorhill = await getGorhillPublicSuffixPromise();
  205. const deduped = span.traceChild('sort result').traceSyncFn(() => sortDomains(domainDeduper(Array.from(domains)), gorhill));
  206. const description = [
  207. ...SHARED_DESCRIPTION,
  208. '',
  209. 'This file contains common speedtest endpoints.'
  210. ];
  211. return Promise.all(createRuleset(
  212. 'Sukka\'s Ruleset - Speedtest Domains',
  213. description,
  214. new Date(),
  215. deduped,
  216. 'domainset',
  217. path.resolve(import.meta.dir, '../List/domainset/speedtest.conf'),
  218. path.resolve(import.meta.dir, '../Clash/domainset/speedtest.txt')
  219. ));
  220. });
  221. if (import.meta.main) {
  222. buildSpeedtestDomainSet();
  223. }