build-speedtest-domainset.ts 6.6 KB

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