validate-gfwlist.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { processLine } from './lib/process-line';
  2. import { fastNormalizeDomain } from './lib/normalize-domain';
  3. import { HostnameSmolTrie } from 'hntrie/smol';
  4. import { domainToASCII } from 'node:url';
  5. import yauzl from 'yauzl-promise';
  6. import { fetchRemoteTextByLine } from './lib/fetch-text-by-line';
  7. import path from 'node:path';
  8. import { OUTPUT_SURGE_DIR, SOURCE_DIR } from './constants/dir';
  9. import { createRetrieKeywordFilter as createKeywordFilter } from 'foxts/retrie';
  10. import { $$fetch } from './lib/fetch-retry';
  11. import runAgainstSourceFile from './lib/run-against-source-file';
  12. import { nullthrow } from 'foxts/guard';
  13. import { Buffer } from 'node:buffer';
  14. import { GLOBAL } from '../Source/non_ip/global';
  15. export async function getTopOneMillionDomains() {
  16. const { parse: csvParser } = await import('csv-parse');
  17. const topDomainTrie = new HostnameSmolTrie();
  18. const csvParse = csvParser({ columns: false, skip_empty_lines: true });
  19. const topDomainsZipBody = await (await $$fetch('https://tranco-list.eu/top-1m.csv.zip', {
  20. headers: {
  21. accept: '*/*',
  22. 'user-agent': 'curl/8.12.1'
  23. }
  24. })).arrayBuffer();
  25. let entry: yauzl.Entry | null = null;
  26. for await (const e of await yauzl.fromBuffer(Buffer.from(topDomainsZipBody))) {
  27. if (e.filename === 'top-1m.csv') {
  28. entry = e;
  29. break;
  30. }
  31. }
  32. const { promise, resolve, reject } = Promise.withResolvers<HostnameSmolTrie>();
  33. const readable = await nullthrow(entry, 'top-1m.csv entry not found').openReadStream();
  34. const parser = readable.pipe(csvParse);
  35. parser.on('readable', () => {
  36. let record;
  37. while ((record = parser.read()) !== null) {
  38. topDomainTrie.add(record[1]);
  39. }
  40. });
  41. parser.on('end', () => {
  42. resolve(topDomainTrie);
  43. });
  44. parser.on('error', (err) => {
  45. reject(err);
  46. });
  47. return promise;
  48. }
  49. export async function parseGfwList() {
  50. const whiteSet = new Set<string>();
  51. const gfwListTrie = new HostnameSmolTrie();
  52. let totalGfwSize = 0;
  53. const gfwlistIgnoreLineKwfilter = createKeywordFilter([
  54. '.*',
  55. '*',
  56. '=',
  57. '[',
  58. '/',
  59. '?'
  60. ]);
  61. const text = await (await $$fetch('https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt')).text();
  62. for (const l of atob(text).split('\n')) {
  63. const line = processLine(l);
  64. if (!line) continue;
  65. if (gfwlistIgnoreLineKwfilter(line)) {
  66. continue;
  67. }
  68. if (line.startsWith('@@||')) {
  69. whiteSet.add('.' + line.slice(4));
  70. continue;
  71. }
  72. if (line.startsWith('@@|http://')) {
  73. whiteSet.add(line.slice(10));
  74. continue;
  75. }
  76. if (line.startsWith('@@|https://')) {
  77. whiteSet.add(line.slice(11));
  78. continue;
  79. }
  80. if (line.startsWith('||')) {
  81. gfwListTrie.add('.' + line.slice(2));
  82. continue;
  83. }
  84. if (line[0] === '|') {
  85. gfwListTrie.add(line.slice(1));
  86. continue;
  87. }
  88. if (line[0] === '.') {
  89. gfwListTrie.add(line);
  90. continue;
  91. }
  92. const d = fastNormalizeDomain(line);
  93. if (d) {
  94. totalGfwSize++;
  95. gfwListTrie.add(d);
  96. }
  97. }
  98. for await (const l of await fetchRemoteTextByLine('https://raw.githubusercontent.com/Loyalsoldier/cn-blocked-domain/release/domains.txt', true)) {
  99. totalGfwSize++;
  100. gfwListTrie.add(l);
  101. }
  102. for await (const l of await fetchRemoteTextByLine('https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt', true)) {
  103. totalGfwSize++;
  104. gfwListTrie.add(l);
  105. }
  106. const topDomainTrie = await getTopOneMillionDomains();
  107. const keywordSet = new Set<string>();
  108. const callback = (domain: string, includeAllSubdomain: boolean) => {
  109. const d = includeAllSubdomain ? '.' + domain : domain;
  110. gfwListTrie.whitelist(d);
  111. topDomainTrie.whitelist(d);
  112. };
  113. await Promise.all([
  114. runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip/global.conf'), callback, 'ruleset', keywordSet),
  115. // runAgainstSourceFile(path.join(OUTPUT_SURGE_DIR, 'non_ip/domestic.conf'), callback, 'ruleset', keywordSet),
  116. runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip/reject.conf'), callback, 'ruleset', keywordSet),
  117. runAgainstSourceFile(path.join(SOURCE_DIR, 'non_ip/telegram.conf'), callback, 'ruleset', keywordSet),
  118. runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'non_ip/stream.conf'), callback, 'ruleset', keywordSet),
  119. runAgainstSourceFile(path.resolve(SOURCE_DIR, 'non_ip/ai.conf'), callback, 'ruleset', keywordSet),
  120. runAgainstSourceFile(path.resolve(SOURCE_DIR, 'non_ip/microsoft.conf'), callback, 'ruleset', keywordSet),
  121. runAgainstSourceFile(path.resolve(SOURCE_DIR, 'non_ip/apple_services.conf'), callback, 'ruleset', keywordSet),
  122. runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'domainset/reject.conf'), callback, 'domainset'),
  123. runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'domainset/reject_extra.conf'), callback, 'domainset'),
  124. runAgainstSourceFile(path.resolve(OUTPUT_SURGE_DIR, 'domainset/cdn.conf'), callback, 'domainset')
  125. ]);
  126. Object.values(GLOBAL).forEach(({ domains }) => {
  127. domains.forEach(domain => {
  128. if (domain[0] === '$') {
  129. callback(domain.slice(1), false);
  130. } else if (domain[0] === '+') {
  131. callback(domain.slice(1), true);
  132. } else {
  133. callback(domain, true);
  134. }
  135. });
  136. });
  137. whiteSet.forEach(domain => gfwListTrie.whitelist(domain[0] === '.' ? domain : '.' + domain));
  138. let dedupedGfwListSize = 0;
  139. gfwListTrie.dump(() => { dedupedGfwListSize++; });
  140. const kwfilter = createKeywordFilter([...keywordSet]);
  141. const missingTop10000Gfwed = new Set<string>();
  142. topDomainTrie.dump((rawDomain) => {
  143. const domain = domainToASCII(rawDomain);
  144. if (domain && gfwListTrie.match(domain) && !kwfilter(domain)) {
  145. missingTop10000Gfwed.add(domain);
  146. }
  147. });
  148. console.log(Array.from(missingTop10000Gfwed).join('\n'));
  149. console.log({ totalGfwSize, dedupedGfwListSize, missingSize: missingTop10000Gfwed.size });
  150. return [
  151. whiteSet,
  152. gfwListTrie,
  153. missingTop10000Gfwed
  154. ] as const;
  155. }
  156. if (require.main === module) {
  157. parseGfwList().catch(console.error);
  158. }
  159. // python.com waiting-for-sell