build-speedtest-domainset.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import path from 'node:path';
  2. import { Sema } from 'async-sema';
  3. import { getHostname } from 'tldts-experimental';
  4. import { task } from './trace';
  5. import { fetchWithRetry } from './lib/fetch-retry';
  6. import { SHARED_DESCRIPTION } from './lib/constants';
  7. import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
  8. import { TTL, deserializeArray, fsFetchCache, serializeArray, createCacheKey } from './lib/cache-filesystem';
  9. import { DomainsetOutput } from './lib/create-file';
  10. import { OUTPUT_SURGE_DIR } from './constants/dir';
  11. const KEYWORDS = [
  12. 'Hong Kong',
  13. 'Taiwan',
  14. 'China Telecom',
  15. 'China Mobile',
  16. 'China Unicom',
  17. 'Japan',
  18. 'Tokyo',
  19. 'Singapore',
  20. 'Korea',
  21. 'Seoul',
  22. 'Canada',
  23. 'Toronto',
  24. 'Montreal',
  25. 'Los Ang',
  26. 'San Jos',
  27. 'Seattle',
  28. 'New York',
  29. 'Dallas',
  30. 'Miami',
  31. 'Berlin',
  32. 'Frankfurt',
  33. 'London',
  34. 'Paris',
  35. 'Amsterdam',
  36. 'Moscow',
  37. 'Australia',
  38. 'Sydney',
  39. 'Brazil',
  40. 'Turkey'
  41. ];
  42. const PREDEFINE_DOMAINS = [
  43. // speedtest.net
  44. '.speedtest.net',
  45. '.speedtestcustom.com',
  46. '.ooklaserver.net',
  47. '.speed.misaka.one',
  48. '.speedtest.rt.ru',
  49. '.speedtest.aptg.com.tw',
  50. '.speedtest.gslnetworks.com',
  51. '.speedtest.jsinfo.net',
  52. '.speedtest.i3d.net',
  53. '.speedtestkorea.com',
  54. '.speedtest.telus.com',
  55. '.speedtest.telstra.net',
  56. '.speedtest.clouvider.net',
  57. '.speedtest.idv.tw',
  58. '.speedtest.frontier.com',
  59. '.speedtest.orange.fr',
  60. '.speedtest.centurylink.net',
  61. '.srvr.bell.ca',
  62. '.speedtest.contabo.net',
  63. 'speedtest.hk.chinamobile.com',
  64. 'speedtestbb.hk.chinamobile.com',
  65. '.hizinitestet.com',
  66. '.linknetspeedtest.net.br',
  67. 'speedtest.rit.edu',
  68. 'speedtest.ropa.de',
  69. 'speedtest.sits.su',
  70. 'speedtest.tigo.cr',
  71. 'speedtest.upp.com',
  72. '.speedtest.pni.tw',
  73. '.speed.pfm.gg',
  74. '.speedtest.faelix.net',
  75. '.speedtest.labixe.net',
  76. '.speedtest.warian.net',
  77. '.speedtest.starhub.com',
  78. '.speedtest.gibir.net.tr',
  79. '.speedtest.ozarksgo.net',
  80. '.speedtest.exetel.com.au',
  81. '.speedtest.sbcglobal.net',
  82. '.speedtest.leaptel.com.au',
  83. '.speedtest.windstream.net',
  84. '.speedtest.vodafone.com.au',
  85. '.speedtest.rascom.ru',
  86. '.speedtest.dchost.com',
  87. '.speedtest.highnet.com',
  88. '.speedtest.seattle.wa.limewave.net',
  89. '.speedtest.optitel.com.au',
  90. '.speednet.net.tr',
  91. '.speedtest.angolacables.co.ao',
  92. '.ookla-speedtest.fsr.com',
  93. '.speedtest.comnet.com.tr',
  94. '.speedtest.gslnetworks.com.au',
  95. '.test.gslnetworks.com.au',
  96. '.speedtest.gslnetworks.com',
  97. '.speedtestunonet.com.br',
  98. '.speedtest.alagas.net',
  99. 'speedtest.surfshark.com',
  100. '.speedtest.aarnet.net.au',
  101. '.ookla.rcp.net',
  102. '.ookla-speedtests.e2ro.com',
  103. '.speedtest.com.sg',
  104. '.ookla.ddnsgeek.com',
  105. '.speedtest.pni.tw',
  106. '.speedtest.cmcnetworks.net',
  107. '.speedtestwnet.com.br',
  108. // Cloudflare
  109. '.speed.cloudflare.com',
  110. // Wi-Fi Man
  111. '.wifiman.com',
  112. '.wifiman.me',
  113. '.wifiman.ubncloud.com',
  114. '.wifiman-stability-test.ubncloud.com',
  115. // Fast.com
  116. '.fast.com',
  117. // MacPaw
  118. 'speedtest.macpaw.com',
  119. // speedtestmaster
  120. '.netspeedtestmaster.com',
  121. // Google Search Result of "speedtest", powered by this
  122. '.measurement-lab.org',
  123. '.measurementlab.net',
  124. // Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com)
  125. '.speed.googlefiber.net',
  126. // librespeed
  127. '.backend.librespeed.org',
  128. // Apple,
  129. 'mensura.cdn-apple.com', // From netQuality command
  130. // OpenSpeedtest
  131. 'open.cachefly.net' // This is also used for openspeedtest server download
  132. ];
  133. const s = new Sema(2);
  134. const cacheKey = createCacheKey(__filename);
  135. const latestTopUserAgentsPromise = fsFetchCache.apply(
  136. cacheKey('https://cdn.jsdelivr.net/npm/top-user-agents@latest/src/desktop.json'),
  137. () => fetchWithRetry(
  138. 'https://cdn.jsdelivr.net/npm/top-user-agents@latest/src/desktop.json',
  139. { signal: AbortSignal.timeout(1000 * 60) }
  140. )
  141. .then(res => res.json() as Promise<string[]>)
  142. .then((userAgents) => userAgents.filter(ua => ua.startsWith('Mozilla/5.0 '))),
  143. {
  144. serializer: serializeArray,
  145. deserializer: deserializeArray,
  146. ttl: TTL.THREE_DAYS()
  147. }
  148. );
  149. const querySpeedtestApi = async (keyword: string): Promise<Array<string | null>> => {
  150. const topUserAgents = await latestTopUserAgentsPromise;
  151. const url = `https://www.speedtest.net/api/js/servers?engine=js&search=${keyword}&limit=100`;
  152. try {
  153. const randomUserAgent = topUserAgents[Math.floor(Math.random() * topUserAgents.length)];
  154. return await fsFetchCache.apply(
  155. cacheKey(url),
  156. () => s.acquire().then(() => fetchWithRetry(url, {
  157. headers: {
  158. dnt: '1',
  159. Referer: 'https://www.speedtest.net/',
  160. accept: 'application/json, text/plain, */*',
  161. 'User-Agent': randomUserAgent,
  162. 'Accept-Language': 'en-US,en;q=0.9',
  163. ...(randomUserAgent.includes('Chrome')
  164. ? {
  165. 'Sec-Ch-Ua-Mobile': '?0',
  166. 'Sec-Fetch-Dest': 'empty',
  167. 'Sec-Fetch-Mode': 'cors',
  168. 'Sec-Fetch-Site': 'same-origin',
  169. 'Sec-Gpc': '1'
  170. }
  171. : {})
  172. },
  173. signal: AbortSignal.timeout(1000 * 60),
  174. retry: {
  175. retries: 2
  176. }
  177. })).then(r => r.json() as any).then((data: Array<{ url: string, host: string }>) => data.reduce<string[]>(
  178. (prev, cur) => {
  179. const line = cur.host || cur.url;
  180. const hn = getHostname(line, { detectIp: false, validateHostname: true });
  181. if (hn) {
  182. prev.push(hn);
  183. }
  184. return prev;
  185. }, []
  186. )).finally(() => s.release()),
  187. {
  188. ttl: TTL.ONE_WEEK(),
  189. serializer: serializeArray,
  190. deserializer: deserializeArray
  191. }
  192. );
  193. } catch (e) {
  194. console.error(e);
  195. return [];
  196. }
  197. };
  198. export const buildSpeedtestDomainSet = task(require.main === module, __filename)(async (span) => {
  199. const output = new DomainsetOutput(span, 'speedtest')
  200. .withTitle('Sukka\'s Ruleset - Speedtest Domains')
  201. .withDescription([
  202. ...SHARED_DESCRIPTION,
  203. '',
  204. 'This file contains common speedtest endpoints.'
  205. ])
  206. .addFromDomainset(PREDEFINE_DOMAINS)
  207. .addFromDomainset(await readFileIntoProcessedArray(path.resolve(OUTPUT_SURGE_DIR, 'domainset/speedtest.conf')));
  208. await Promise.all(KEYWORDS.map((keyword) => span.traceChildAsync(
  209. `fetch speedtest endpoints: ${keyword}`,
  210. () => querySpeedtestApi(keyword)
  211. ).then(hostnameGroup => hostnameGroup.forEach(hostname => {
  212. if (hostname) {
  213. output.addDomain(hostname);
  214. }
  215. }))));
  216. return output.write();
  217. });