build-speedtest-domainset.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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 { createMemoizedPromise } from './lib/memo-promise';
  14. import { setAddFromArrayCurried } from './lib/set-add-from-array';
  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 any)
  20. .then((userAgents: string[]) => 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 }>) => data.reduce<string[]>(
  56. (prev, cur) => {
  57. const hn = 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. const getPreviousSpeedtestDomainsPromise = createMemoizedPromise(async () => {
  76. try {
  77. return await readFileIntoProcessedArray(path.resolve(import.meta.dir, '../List/domainset/speedtest.conf'));
  78. } catch {
  79. return [];
  80. }
  81. });
  82. export const buildSpeedtestDomainSet = task(import.meta.main, import.meta.path)(async (span) => {
  83. // Predefined domainset
  84. /** @type {Set<string>} */
  85. const domains = new Set<string>([
  86. // speedtest.net
  87. '.speedtest.net',
  88. '.speedtestcustom.com',
  89. '.ooklaserver.net',
  90. '.speed.misaka.one',
  91. '.speedtest.rt.ru',
  92. '.speedtest.aptg.com.tw',
  93. '.speedtest.gslnetworks.com',
  94. '.speedtest.jsinfo.net',
  95. '.speedtest.i3d.net',
  96. '.speedtestkorea.com',
  97. '.speedtest.telus.com',
  98. '.speedtest.telstra.net',
  99. '.speedtest.clouvider.net',
  100. '.speedtest.idv.tw',
  101. '.speedtest.frontier.com',
  102. '.speedtest.orange.fr',
  103. '.speedtest.centurylink.net',
  104. '.srvr.bell.ca',
  105. '.speedtest.contabo.net',
  106. 'speedtest.hk.chinamobile.com',
  107. 'speedtestbb.hk.chinamobile.com',
  108. '.hizinitestet.com',
  109. '.linknetspeedtest.net.br',
  110. 'speedtest.rit.edu',
  111. 'speedtest.ropa.de',
  112. 'speedtest.sits.su',
  113. 'speedtest.tigo.cr',
  114. 'speedtest.upp.com',
  115. '.speedtest.pni.tw',
  116. '.speed.pfm.gg',
  117. '.speedtest.faelix.net',
  118. '.speedtest.labixe.net',
  119. '.speedtest.warian.net',
  120. '.speedtest.starhub.com',
  121. '.speedtest.gibir.net.tr',
  122. '.speedtest.ozarksgo.net',
  123. '.speedtest.exetel.com.au',
  124. '.speedtest.sbcglobal.net',
  125. '.speedtest.leaptel.com.au',
  126. '.speedtest.windstream.net',
  127. '.speedtest.vodafone.com.au',
  128. '.speedtest.rascom.ru',
  129. '.speedtest.dchost.com',
  130. '.speedtest.highnet.com',
  131. '.speedtest.seattle.wa.limewave.net',
  132. '.speedtest.optitel.com.au',
  133. '.speednet.net.tr',
  134. '.speedtest.angolacables.co.ao',
  135. '.ookla-speedtest.fsr.com',
  136. '.speedtest.comnet.com.tr',
  137. '.speedtest.gslnetworks.com.au',
  138. '.test.gslnetworks.com.au',
  139. '.speedtest.gslnetworks.com',
  140. '.speedtestunonet.com.br',
  141. '.speedtest.alagas.net',
  142. 'speedtest.surfshark.com',
  143. '.speedtest.aarnet.net.au',
  144. '.ookla.rcp.net',
  145. '.ookla-speedtests.e2ro.com',
  146. '.speedtest.com.sg',
  147. '.ookla.ddnsgeek.com',
  148. '.speedtest.pni.tw',
  149. // Cloudflare
  150. '.speed.cloudflare.com',
  151. // Wi-Fi Man
  152. '.wifiman.com',
  153. '.wifiman.me',
  154. '.wifiman.ubncloud.com',
  155. // Fast.com
  156. '.fast.com',
  157. // MacPaw
  158. 'speedtest.macpaw.com',
  159. // speedtestmaster
  160. '.netspeedtestmaster.com',
  161. // Google Search Result of "speedtest", powered by this
  162. '.measurement-lab.org',
  163. '.measurementlab.net',
  164. // Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com)
  165. '.speed.googlefiber.net',
  166. // librespeed
  167. '.backend.librespeed.org',
  168. // Apple,
  169. 'mensura.cdn-apple.com', // From netQuality command
  170. // OpenSpeedtest
  171. 'open.cachefly.net'
  172. ]);
  173. await span.traceChildAsync(
  174. 'fetch previous speedtest domainset',
  175. () => getPreviousSpeedtestDomainsPromise()
  176. .then(setAddFromArrayCurried(domains))
  177. );
  178. await new Promise<void>((resolve, reject) => {
  179. const pMap = ([
  180. 'Hong Kong',
  181. 'Taiwan',
  182. 'China Telecom',
  183. 'China Mobile',
  184. 'China Unicom',
  185. 'Japan',
  186. 'Tokyo',
  187. 'Singapore',
  188. 'Korea',
  189. 'Seoul',
  190. 'Canada',
  191. 'Toronto',
  192. 'Montreal',
  193. 'Los Ang',
  194. 'San Jos',
  195. 'Seattle',
  196. 'New York',
  197. 'Dallas',
  198. 'Miami',
  199. 'Berlin',
  200. 'Frankfurt',
  201. 'London',
  202. 'Paris',
  203. 'Amsterdam',
  204. 'Moscow',
  205. 'Australia',
  206. 'Sydney',
  207. 'Brazil',
  208. 'Turkey'
  209. ]).reduce<Record<string, Promise<void>>>((pMap, keyword) => {
  210. pMap[keyword] = span.traceChildAsync(`fetch speedtest endpoints: ${keyword}`, () => querySpeedtestApi(keyword)).then(hostnameGroup => {
  211. return hostnameGroup.forEach(hostname => {
  212. if (hostname) {
  213. domains.add(hostname);
  214. }
  215. });
  216. });
  217. return pMap;
  218. }, {});
  219. const timer = setTimeout(() => {
  220. console.error(picocolors.red('Task timeout!'));
  221. Object.entries(pMap).forEach(([name, p]) => {
  222. console.log(`[${name}]`, Bun.peek.status(p));
  223. });
  224. resolve();
  225. }, 1000 * 60 * 1.5);
  226. Promise.all(Object.values(pMap)).then(() => {
  227. clearTimeout(timer);
  228. return resolve();
  229. }).catch(() => reject);
  230. });
  231. const deduped = span.traceChildSync('sort result', () => sortDomains(domainDeduper(Array.from(domains))));
  232. const description = [
  233. ...SHARED_DESCRIPTION,
  234. '',
  235. 'This file contains common speedtest endpoints.'
  236. ];
  237. return createRuleset(
  238. span,
  239. 'Sukka\'s Ruleset - Speedtest Domains',
  240. description,
  241. new Date(),
  242. deduped,
  243. 'domainset',
  244. path.resolve(import.meta.dir, '../List/domainset/speedtest.conf'),
  245. path.resolve(import.meta.dir, '../Clash/domainset/speedtest.txt')
  246. );
  247. });