block-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.eslintignore +3 -0
  2. package/AD_BLOCK.md +38 -0
  3. package/Dockerfile +51 -0
  4. package/LICENSE +21 -0
  5. package/README.md +182 -0
  6. package/bin/start.js +45 -0
  7. package/cert/rootCA.crt +20 -0
  8. package/cert/rootCA.key +27 -0
  9. package/config.json +234 -0
  10. package/craco.config.js +52 -0
  11. package/hack-of-anyproxy/lib/requestHandler.js +1028 -0
  12. package/package.json +54 -0
  13. package/proxy/attacker.js +135 -0
  14. package/proxy/domain.js +26 -0
  15. package/proxy/fs.js +46 -0
  16. package/proxy/http.js +224 -0
  17. package/proxy/mitm/persistentStore.js +34 -0
  18. package/proxy/mitm/persistentStore.json +3 -0
  19. package/proxy/mitm/rule.js +116 -0
  20. package/proxy/mitm/uaFilter.js +47 -0
  21. package/proxy/mitm/ydcd/ydcd.js +34 -0
  22. package/proxy/mitm/youtube/youtube.response.js +39 -0
  23. package/proxy/monitor.js +283 -0
  24. package/proxy/proxy.js +1488 -0
  25. package/proxy/scan.js +120 -0
  26. package/proxy/start.js +7 -0
  27. package/proxy/wanip.js +76 -0
  28. package/public/favicon.ico +0 -0
  29. package/public/index.html +43 -0
  30. package/public/iphone-proxy-setting.jpg +0 -0
  31. package/public/logo192.png +0 -0
  32. package/public/logo512.png +0 -0
  33. package/public/manifest.json +25 -0
  34. package/public/proxy.jpg +0 -0
  35. package/public/robots.txt +3 -0
  36. package/server/express.js +232 -0
  37. package/server/start.js +24 -0
  38. package/server/util.js +166 -0
  39. package/socks5/server.js +321 -0
  40. package/socks5/start.js +7 -0
  41. package/socks5/test_tls_reuse.js +40 -0
  42. package/src/App.css +505 -0
  43. package/src/App.js +759 -0
  44. package/src/App.test.js +8 -0
  45. package/src/index.css +13 -0
  46. package/src/index.js +17 -0
  47. package/src/logo.svg +1 -0
  48. package/src/reportWebVitals.js +13 -0
  49. package/src/setupTests.js +5 -0
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "block-proxy",
3
+ "version": "0.1.0",
4
+ "description": "Small-scale network proxy filter",
5
+ "bin":{
6
+ "block-proxy": "./bin/start.js"
7
+ },
8
+ "dependencies": {
9
+ "react": "^19.2.0",
10
+ "react-dom": "^19.2.0",
11
+ "react-scripts": "5.0.1",
12
+ "web-vitals": "^2.1.4"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/jayli/block-proxy.git"
17
+ },
18
+ "scripts": {
19
+ "cp": "cp ./hack-of-anyproxy/lib/requestHandler.js ./node_modules/anyproxy/lib/",
20
+ "craco": "craco start",
21
+ "dev": "BLOCK_PROXY_DEV=1 npm run express",
22
+ "start": "npm run express",
23
+ "socks5": "node ./socks5/start.js",
24
+ "express": "npm run cp && node ./server/start.js",
25
+ "proxy": "npm run cp && node ./proxy/start.js",
26
+ "build": "react-scripts build",
27
+ "docker:build": "npm run build && docker build -t block-proxy .",
28
+ "docker:build_arm": "npm run build && docker buildx build --platform linux/arm64/v8 -t block-proxy .",
29
+ "test": "react-scripts test",
30
+ "eject": "react-scripts eject"
31
+ },
32
+ "devDependencies": {
33
+ "@craco/craco": "^7.1.0",
34
+ "anyproxy": "^4.1.3",
35
+ "axios": "^1.13.2",
36
+ "express": "^5.1.0",
37
+ "http-proxy-agent": "^7.0.2",
38
+ "https-proxy-agent": "^7.0.6",
39
+ "ping": "^1.0.0",
40
+ "write-file-atomic": "^7.0.0"
41
+ },
42
+ "browserslist": {
43
+ "production": [
44
+ ">0.2%",
45
+ "not dead",
46
+ "not op_mini all"
47
+ ],
48
+ "development": [
49
+ "last 1 chrome version",
50
+ "last 1 firefox version",
51
+ "last 1 safari version"
52
+ ]
53
+ }
54
+ }
@@ -0,0 +1,135 @@
1
+ // proxy/attacker.js
2
+ // 存储每个 IP 的访问信息
3
+ const ipAccessLog = new Map();
4
+ // value: { timestamps: number[], isBad: boolean, lastSeen: number, goodUntil: number | null }
5
+
6
+ const WINDOW_MS = 2 * 60 * 1000; // 2 分钟滑动窗口
7
+ const BAD_THRESHOLD = 20; // 恶意阈值
8
+ const INACTIVE_TIMEOUT = 10 * 60 * 1000; // 普通 IP 10 分钟无活动则清理
9
+ const GOOD_GUY_DURATION = 10 * 60 * 1000; // 好人豁免期:10 分钟
10
+
11
+ /**
12
+ * 记录并返回指定 IP 在过去 2 分钟内的访问次数
13
+ * 如果在“好人豁免期”内,即使高频也不标记为恶意
14
+ */
15
+ function countIPAccess(ip) {
16
+ const now = Date.now();
17
+ const cutoff = now - WINDOW_MS;
18
+
19
+ let record = ipAccessLog.get(ip);
20
+ if (!record) {
21
+ record = {
22
+ timestamps: [],
23
+ isBad: false,
24
+ lastSeen: 0,
25
+ goodUntil: null // 新增字段
26
+ };
27
+ }
28
+
29
+ // 清理过期时间戳
30
+ const recentTimestamps = record.timestamps.filter(ts => ts > cutoff);
31
+ recentTimestamps.push(now);
32
+ const currentCount = recentTimestamps.length;
33
+
34
+ // 🔒 判断是否处于“好人豁免期”
35
+ const isInGoodPeriod = record.goodUntil && now < record.goodUntil;
36
+
37
+ // 只有不在豁免期,才可能被标记为坏人
38
+ const newIsBad = isInGoodPeriod
39
+ ? false
40
+ : (record.isBad || currentCount >= BAD_THRESHOLD);
41
+
42
+ ipAccessLog.set(ip, {
43
+ timestamps: recentTimestamps,
44
+ isBad: newIsBad,
45
+ lastSeen: now,
46
+ goodUntil: record.goodUntil // 保留原有 goodUntil
47
+ });
48
+
49
+ return currentCount;
50
+ }
51
+
52
+ /**
53
+ * 判断是否为恶意 IP
54
+ * 注意:即使 isBad=true,若在 goodUntil 期内,也应视为非恶意(但按你要求,isBad 字段可保留)
55
+ * 但根据你的描述:“只要被标记过是好人就不能被标记为坏人”,我们让 isBad=false 在豁免期内
56
+ * —— 实际上上面 countIPAccess 已确保 isBad 不会为 true
57
+ */
58
+ function isBadGuy(ip) {
59
+ const record = ipAccessLog.get(ip);
60
+ if (!record) return false;
61
+
62
+ const now = Date.now();
63
+ const isInGoodPeriod = record.goodUntil && now < record.goodUntil;
64
+
65
+ // 豁免期内,即使 isBad 字段残留,也返回 false
66
+ return isInGoodPeriod ? false : record.isBad;
67
+ }
68
+
69
+ /**
70
+ * 手动将 IP 标记为“好人”:获得 10 分钟豁免权
71
+ * - 不能被标记为恶意
72
+ * - 记录至少保留 10 分钟(即使无访问)
73
+ */
74
+ function setGoodGuy(ip) {
75
+ const now = Date.now();
76
+ let record = ipAccessLog.get(ip);
77
+
78
+ if (!record) {
79
+ record = {
80
+ timestamps: [],
81
+ isBad: false,
82
+ lastSeen: now,
83
+ goodUntil: null
84
+ };
85
+ }
86
+
87
+ // 设置豁免截止时间
88
+ record.goodUntil = now + GOOD_GUY_DURATION;
89
+ record.lastSeen = now; // 更新 lastSeen 防止被 cleanup 立即删掉
90
+ record.isBad = false; // 立即洗白
91
+
92
+ ipAccessLog.set(ip, record);
93
+ }
94
+
95
+ /**
96
+ * 清理过期记录:
97
+ * - 普通 IP:10 分钟无访问 → 删除
98
+ * - 好人 IP:即使无访问,只要 goodUntil 未过期 → 保留
99
+ */
100
+ function cleanupInactiveIPs() {
101
+ const now = Date.now();
102
+ const inactiveCutoff = now - INACTIVE_TIMEOUT;
103
+
104
+ for (const [ip, record] of ipAccessLog.entries()) {
105
+ const isGoodGuyActive = record.goodUntil && now < record.goodUntil;
106
+
107
+ if (!isGoodGuyActive && record.lastSeen < inactiveCutoff) {
108
+ // 不是活跃好人,且超过 10 分钟无访问 → 删除
109
+ ipAccessLog.delete(ip);
110
+ } else {
111
+ // 清理 timestamps 中的过期项
112
+ const recent = record.timestamps.filter(ts => ts > now - WINDOW_MS);
113
+ if (recent.length !== record.timestamps.length) {
114
+ ipAccessLog.set(ip, { ...record, timestamps: recent });
115
+ }
116
+ }
117
+ }
118
+
119
+ // 在 countIPAccess 中增加硬上限(例如最多保留 10,000 个 IP)
120
+ if (ipAccessLog.size > 10000) {
121
+ // 按 lastSeen 排序,删除最旧的一批
122
+ const sorted = Array.from(ipAccessLog.entries())
123
+ .sort((a, b) => a[1].lastSeen - b[1].lastSeen);
124
+ for (let i = 0; i < 1000; i++) {
125
+ ipAccessLog.delete(sorted[i][0]);
126
+ }
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ countIPAccess,
132
+ isBadGuy,
133
+ setGoodGuy,
134
+ cleanupInactiveIPs
135
+ };
@@ -0,0 +1,26 @@
1
+ // proxy/domain.js
2
+ const { lookup, resolve4 } = require('dns');
3
+ const { promisify } = require('util');
4
+
5
+ const lookupAsync = promisify(lookup);
6
+ const resolve4Async = promisify(resolve4);
7
+
8
+ async function getDomainIP(domain) {
9
+ try {
10
+ const addresses = await resolve4Async(domain);
11
+ return addresses; // 返回数组,如 ['93.184.216.34']
12
+ } catch (err) {
13
+ console.error(`Failed to resolve ${domain}:`, err.message);
14
+ return null;
15
+ }
16
+ }
17
+
18
+ // 使用示例
19
+ /*
20
+ getDomainIP('example.com').then(ips => {
21
+ if (ips) {
22
+ console.log(`${ips.length} IP(s):`, ips.join(', '));
23
+ }
24
+ });
25
+ */
26
+ module.exports.getDomainIP = getDomainIP;
package/proxy/fs.js ADDED
@@ -0,0 +1,46 @@
1
+ // proxy/fs.js
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+ const writeFileAtomic = require('write-file-atomic'); // 引入 write-file-atomic
5
+
6
+ const configPath = path.join(__dirname, '../config.json');
7
+ const CONFIG_FILE_PATH = configPath;
8
+
9
+ // 传入的是对象
10
+ async function writeConfig(newData) {
11
+ try {
12
+ // 使用 write-file-atomic 进行原子写入
13
+ // 它会在内部创建一个临时文件,写入成功后再重命名为目标文件
14
+ await writeFileAtomic(CONFIG_FILE_PATH, JSON.stringify(newData, null, 2), 'utf8');
15
+ // console.log('Config file written successfully');
16
+ } catch (error) {
17
+ console.error('Error writing config file:', error.message);
18
+ throw error; // 或者根据需要处理错误
19
+ }
20
+ }
21
+
22
+ // 示例:读取配置
23
+ // 返回的是对象
24
+ async function readConfig() {
25
+ try {
26
+ const data = await fs.readFile(CONFIG_FILE_PATH, 'utf8');
27
+ const config = JSON.parse(data);
28
+ return config;
29
+ } catch (error) {
30
+ if (error.code === 'ENOENT') {
31
+ console.error('Config file does not exist.');
32
+ return {}; // 或返回一个默认配置对象
33
+ } else if (error instanceof SyntaxError) {
34
+ console.error('Error parsing config file JSON:', error.message);
35
+ throw error;
36
+ } else {
37
+ console.error('Error reading config file:', error.message);
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+
43
+ module.exports = {
44
+ writeConfig,
45
+ readConfig
46
+ };
package/proxy/http.js ADDED
@@ -0,0 +1,224 @@
1
+ // proxy/http.js
2
+ const net = require('net');
3
+
4
+ /**
5
+ * 发送 HTTP 请求并返回完整响应(绕过头校验)
6
+ * @param {Object} options - 同 http.request 的 options
7
+ * @returns {Promise<{ statusCode: number, headers: Object, body: string }>}
8
+ */
9
+ function requestNoValidationSync(options) {
10
+ return new Promise((resolve, reject) => {
11
+ const method = (options.method || 'GET').toUpperCase();
12
+ const hostname = options.hostname || options.host || 'localhost';
13
+ const port = options.port || 80;
14
+ const path = options.path || '/';
15
+ const headers = options.headers || {};
16
+
17
+ // 构造 Host 头
18
+ let host = hostname;
19
+ if (port !== 80) host += ':' + port;
20
+ const finalHeaders = { ...headers };
21
+ if (!('Host' in finalHeaders) && !('host' in finalHeaders)) {
22
+ finalHeaders.Host = host;
23
+ }
24
+
25
+ // 构造请求头
26
+ let headerStr = `${method} ${path} HTTP/1.1\r\n`;
27
+ for (const [key, val] of Object.entries(finalHeaders)) {
28
+ headerStr += `${key}: ${val}\r\n`;
29
+ }
30
+ headerStr += '\r\n';
31
+
32
+ const socket = net.connect(port, hostname);
33
+
34
+ const handleError = (err) => {
35
+ socket.destroy();
36
+ reject(err);
37
+ };
38
+
39
+ socket.on('error', handleError);
40
+
41
+ let buffer = Buffer.alloc(0);
42
+ let responseParsed = false;
43
+ let statusCode, statusMessage, headersObj, rawHeaders;
44
+ let bodyChunks = [];
45
+ let expectedBodyLength = null;
46
+ let receivedBodyLength = 0;
47
+
48
+ socket.write(headerStr);
49
+
50
+ // 如果有请求体(简单支持字符串)
51
+ if (options.body) {
52
+ socket.write(options.body);
53
+ }
54
+ // 注意:不要在这里调用 socket.end(),避免过早关闭连接
55
+
56
+ socket.on('data', (chunk) => {
57
+ if (!responseParsed) {
58
+ buffer = Buffer.concat([buffer, chunk]);
59
+ const headerEndIndex = buffer.indexOf('\r\n\r\n');
60
+ if (headerEndIndex === -1) return;
61
+
62
+ const headerBytes = buffer.slice(0, headerEndIndex);
63
+ // 修正: 只需要跳过 \r\n\r\n (4 bytes) 而不是8 bytes
64
+ const bodyStart = headerEndIndex + 4;
65
+ const bodyChunk = buffer.slice(bodyStart);
66
+
67
+ const lines = headerBytes.toString('ascii').split('\r\n');
68
+ const statusLine = lines[0];
69
+ const match = statusLine.match(/^HTTP\/[10]\.[10]\s+(\d{3})\s*(.*)$/i);
70
+ if (!match) {
71
+ return handleError(new Error('Invalid HTTP status line'));
72
+ }
73
+
74
+ statusCode = parseInt(match[1], 10);
75
+ statusMessage = match[2] || '';
76
+
77
+ // 手动解析头部(不校验冲突!)
78
+ headersObj = {};
79
+ rawHeaders = [];
80
+ for (let i = 1; i < lines.length; i++) {
81
+ const line = lines[i];
82
+ if (!line) continue;
83
+ const idx = line.indexOf(':');
84
+ if (idx === -1) continue;
85
+ const key = line.slice(0, idx).trim();
86
+ const val = line.slice(idx + 1).trim();
87
+ const lowerKey = key.toLowerCase();
88
+ rawHeaders.push(key, val);
89
+ if (headersObj[lowerKey] === undefined) {
90
+ headersObj[lowerKey] = val;
91
+ } else if (Array.isArray(headersObj[lowerKey])) {
92
+ headersObj[lowerKey].push(val);
93
+ } else {
94
+ headersObj[lowerKey] = [headersObj[lowerKey], val];
95
+ }
96
+ }
97
+
98
+ // 获取Content-Length
99
+ const contentLength = headersObj['content-length'];
100
+ if (contentLength !== undefined) {
101
+ expectedBodyLength = parseInt(contentLength, 10);
102
+ }
103
+
104
+ responseParsed = true;
105
+
106
+ if (expectedBodyLength == 0) {
107
+ socket.end();
108
+ }
109
+
110
+ // 处理初始的bodyChunk(如果有)
111
+ if (bodyChunk.length > 0) {
112
+ // 检查是否为chunked编码
113
+ const transferEncoding = headersObj['transfer-encoding'];
114
+ if (transferEncoding && transferEncoding.includes('chunked')) {
115
+ // 对于chunked数据,我们将其存储为原始数据,在end事件中统一处理
116
+ bodyChunks.push(bodyChunk);
117
+ receivedBodyLength += bodyChunk.length;
118
+ } else {
119
+ // 非chunked数据按原来的方式处理
120
+ bodyChunks.push(bodyChunk);
121
+ receivedBodyLength += bodyChunk.length;
122
+ }
123
+ }
124
+
125
+ // 如果已知内容长度且已经接收完毕,则关闭连接
126
+ if (expectedBodyLength !== null && receivedBodyLength >= expectedBodyLength) {
127
+ socket.end();
128
+ }
129
+ } else {
130
+ // 已经解析了响应头,正在接收响应体
131
+ bodyChunks.push(chunk);
132
+ receivedBodyLength += chunk.length;
133
+
134
+ // 检查是否已接收完整的响应体
135
+ if (expectedBodyLength !== null && receivedBodyLength >= expectedBodyLength) {
136
+ socket.end();
137
+ }
138
+ }
139
+ });
140
+
141
+ socket.on('end', () => {
142
+ // 检查是否为分块传输编码
143
+ const transferEncoding = headersObj['transfer-encoding'];
144
+ let finalBody = '';
145
+
146
+ if (transferEncoding && transferEncoding.includes('chunked')) {
147
+ // 对每个buffer分别进行解码
148
+ for (const chunkBuffer of bodyChunks) {
149
+ const chunkString = chunkBuffer.toString('utf8');
150
+ let decodedChunk = '';
151
+ let position = 0;
152
+
153
+ while (position < chunkString.length) {
154
+ // 查找当前块大小行的结束位置
155
+ let lineEnding = chunkString.indexOf('\r\n', position);
156
+ if (lineEnding === -1) break;
157
+
158
+ // 提取块大小(十六进制字符串)
159
+ const chunkSizeHex = chunkString.substring(position, lineEnding).trim();
160
+ const chunkSize = parseInt(chunkSizeHex, 16);
161
+
162
+ // 如果块大小为0,表示这是最后一个块
163
+ if (chunkSize === 0) {
164
+ break;
165
+ }
166
+
167
+ // 块数据开始位置
168
+ const dataStart = lineEnding + 2;
169
+ // 块数据结束位置
170
+ const dataEnd = dataStart + chunkSize;
171
+
172
+ // 确保我们没有超出边界
173
+ if (dataEnd <= chunkString.length) {
174
+ // 提取块数据
175
+ decodedChunk += chunkString.substring(dataStart, dataEnd);
176
+ // 移动到下一个块的大小行
177
+ position = dataEnd + 2; // 跳过数据后的 \r\n
178
+ } else {
179
+ // 数据不完整,跳出循环
180
+ break;
181
+ }
182
+ }
183
+
184
+ // 将解码后的chunk添加到最终结果中
185
+ finalBody += decodedChunk;
186
+ }
187
+
188
+ body = finalBody;
189
+ } else if (expectedBodyLength != 0) {
190
+ // 非chunked编码,直接连接buffers
191
+ const bodyBuffer = Buffer.concat(bodyChunks);
192
+ body = bodyBuffer.toString('utf8'); // 默认 UTF-8
193
+
194
+ // 尝试修复可能的响应体问题
195
+ // 如果body以数字开头,可能是Content-Length的值残留
196
+ if (/^\d+\s*\n/.test(body)) {
197
+ // 移除开头的数字和换行符
198
+ body = body.replace(/^\d+\s*\n/, '');
199
+ }
200
+ } else {
201
+ body = "";
202
+ }
203
+
204
+ resolve({
205
+ statusCode,
206
+ statusMessage,
207
+ headers: headersObj,
208
+ body
209
+ });
210
+ });
211
+
212
+ socket.on('close', (hadError) => {
213
+ if (!responseParsed) {
214
+ reject(new Error('Socket closed before response'));
215
+ } else if (hadError && !responseParsed) {
216
+ reject(new Error('Socket closed unexpectedly'));
217
+ }
218
+ });
219
+ });
220
+ }
221
+
222
+ module.exports = {
223
+ request: requestNoValidationSync
224
+ };
@@ -0,0 +1,34 @@
1
+ // proxy/mitm/persistentStore.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ // 存储文件路径(可自定义)
6
+ const STORE_PATH = path.join(__dirname, './persistentStore.json');
7
+
8
+ // 确保存储文件存在
9
+ if (!fs.existsSync(STORE_PATH)) {
10
+ fs.writeFileSync(STORE_PATH, '{}');
11
+ }
12
+
13
+ function read(key) {
14
+ const data = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
15
+ return data[key] || null;
16
+ }
17
+
18
+ function write(value, key) {
19
+ const data = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
20
+ data[key] = value;
21
+ fs.writeFileSync(STORE_PATH, JSON.stringify(data, null, 2));
22
+ }
23
+
24
+ function remove(key) {
25
+ const data = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
26
+ delete data[key];
27
+ fs.writeFileSync(STORE_PATH, JSON.stringify(data, null, 2));
28
+ }
29
+
30
+ module.exports = {
31
+ read,
32
+ write,
33
+ remove
34
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "YouTubeAdvertiseInfo": "{\"version\":\"1.0\",\"whiteNo\":[474376530,79129962,48602820],\"blackNo\":[],\"whiteEml\":[\"cell_divider.eml-fe\",\"inline_injection_entrypoint_layout.eml-fe\",\"video_lockup_with_attachment.eml-fe\",\"images_post_root_slim.eml-fe\",\"spacing_cell.eml-fe\",\"video_metadata_carousel.eml-fe\",\"chips_shelf.eml-js-fe\",\"horizontal_shelf.eml-fe\",\"inbox_notification.eml-fe\",\"expandable_inbox_notification.eml-fe\",\"promo_notification.eml-fe\",\"compact_channel.eml-js-fe\",\"brand_video_singleton.eml-fe\",\"images_post_responsive_root.eml-fe\",\"statement_banner.eml-fe\",\"community_guidelines.eml-fe\",\"text_post_root_slim.eml-fe\"],\"blackEml\":[\"inline_injection_entrypoint_layout.eml\",\"video_display_carousel_buttoned_layout.eml-fe\",\"video_display_button_group_layout.eml-fe\",\"full_width_square_image_layout.eml-fe\",\"video_display_full_buttoned_layout.eml-fe\",\"full_width_square_image_carousel_layout.eml-fe\",\"video_display_full_layout.eml-fe\",\"full_width_portrait_image_layout.eml-fe\",\"video_display_full_buttoned_short_dr_layout.eml-fe\",\"display_tracking.eml-fe\",\"carousel_footered_layout.eml-fe\",\"video_display_carousel_button_group_layout.eml-fe\",\"landscape_image_wide_button_layout.eml-fe\"]}"
3
+ }
@@ -0,0 +1,116 @@
1
+ // proxy/mitm/rule.js
2
+
3
+ const YoutubeResponse = require('./youtube/youtube.response.js');
4
+ const YDCD = require('./ydcd/ydcd.js');
5
+
6
+ function getContentLength(body) {
7
+ let contentLength = 0;
8
+
9
+ // 1. 检查是否为 Uint8Array 类型
10
+ if (body instanceof Uint8Array) {
11
+ contentLength = body.length; // Uint8Array 的 length 就是字节数
12
+ }
13
+ // 2. 检查是否为 Buffer 类型 (注意:Buffer 是 Uint8Array 的子类)
14
+ else if (Buffer.isBuffer(body)) {
15
+ contentLength = body.length;
16
+ }
17
+ // 3. 检查是否为字符串类型
18
+ else if (typeof body === 'string') {
19
+ // 如果是字符串,按 UTF-8 编码转换为字节
20
+ const encoder = new TextEncoder();
21
+ const uint8Array = encoder.encode(body);
22
+ contentLength = uint8Array.length; // 或 uint8Array.byteLength,两者在此处等价
23
+ }
24
+ // 4. 如果都不是,可以返回 0 或根据需要处理其他类型(如 null, undefined, 对象等)
25
+ // 这里保持原样返回 0
26
+
27
+ return contentLength;
28
+ }
29
+
30
+ // 统一劫持 Response 的方法
31
+ function hijackResponse(url, request, response, RuleMojo) {
32
+ return new Promise((resolve, reject) => {
33
+ var responseResult = null;
34
+ RuleMojo.injection({
35
+ callback: function(obj) {
36
+ // console.log('💙💙💙',response);
37
+ var contentLength = 0;
38
+ if (obj.hasOwnProperty('body')) {
39
+ contentLength = getContentLength(obj.body);
40
+ } else {
41
+ contentLength = getContentLength(response.body);
42
+ }
43
+ response.header['Content-Length'] = String(contentLength);
44
+ responseResult = {
45
+ response: {
46
+ statusCode: response.statusCode,
47
+ status: response.statusCode,
48
+ header: response.header,
49
+ body: obj.hasOwnProperty('body') ? obj.body : response.body
50
+ }
51
+ }
52
+ // console.log('>>>>>>>>>>>>>>>>>', request)
53
+ resolve(responseResult);
54
+ },
55
+ url: url,
56
+ response: response,
57
+ request: request
58
+ });
59
+ RuleMojo.main();
60
+ }); // -- promise
61
+ }
62
+
63
+
64
+ // 这里增加规则即可,不用修改 proxy.js
65
+ // callback 被 MITMHandler 调用
66
+ // callback 返回值是一个包含 response 的对象,如果不做处理则返回原 response 里的内容
67
+ module.exports = {
68
+ Youtube: [
69
+ // Rule1
70
+ {
71
+ 'type': 'beforeSendResponse',
72
+ 'host': 'youtubei.googleapis.com',
73
+ 'regexp': "/youtubei/v1/(browse|next|player|search|reel/reel_watch_sequence|guide|account/get_setting|get_watch)",
74
+ 'callback': async function(url, request, response) {
75
+ return await hijackResponse(url, request, response, YoutubeResponse);
76
+ } // -- callback
77
+ },
78
+ // Rule2...
79
+ {
80
+ type: "beforeSendRequest",
81
+ host: "googlevideo.com",
82
+ regexp: "(^https?://[\\w-]+\.googlevideo\.com/.+)(ctier=L)(&.+)",
83
+ callback: async function(url, request, response) {
84
+ const matchRegExp = new RegExp(this.regexp);
85
+ const matchResult = url.match(matchRegExp);
86
+ if (matchResult !== null) {
87
+ const newUrl = matchResult[1] + matchResult[3];
88
+ // console.log(`302 ---------------- ${newUrl}`);
89
+ return {
90
+ response: {
91
+ statusCode: 302,
92
+ header: {
93
+ 'Location': newUrl,
94
+ 'Content-Length': '0'
95
+ },
96
+ body: Buffer.alloc(0)
97
+ }
98
+ };
99
+ } else {
100
+ return null;
101
+ }
102
+ } // -- callback
103
+ },
104
+ ],
105
+ // 有道词典
106
+ YDCD: [
107
+ {
108
+ type: "beforeSendResponse",
109
+ host: "dict.youdao.com",
110
+ regexp: "^https:\/\/dict\.youdao\.com\/vip\/user\/status",
111
+ callback: async function(url, request, response) {
112
+ return await hijackResponse(url, request, response, YDCD);
113
+ }
114
+ }
115
+ ]
116
+ };
@@ -0,0 +1,47 @@
1
+
2
+ const filtered_mitm_domains = [
3
+ "youtube.com",
4
+ "googlevideo.com",
5
+ "youtubei.googleapis.com"
6
+ ];
7
+
8
+ // a.com:443 → a.com
9
+ function trimHost(host) {
10
+ if (/:\d+$/.test(host)) {
11
+ host = host.split(":")[0];
12
+ }
13
+ return host;
14
+ }
15
+
16
+ module.exports = {
17
+ filtered_mitm_domains: filtered_mitm_domains,
18
+ getUA: function(headers) {
19
+ if (headers.hasOwnProperty("User-Agent")) {
20
+ return headers["User-Agent"];
21
+ } else if (headers.hasOwnProperty("user-agent")) {
22
+ return headers["user-agent"];
23
+ } else {
24
+ return "";
25
+ }
26
+ },
27
+ isYoutubeApp: function(ua) {
28
+ if (ua.startsWith("com.google.ios.youtube") || ua.startsWith("com.google.android.youtube")) {
29
+ return true;
30
+ } else {
31
+ return false;
32
+ }
33
+ },
34
+ // 判断是否需要放行
35
+ // 无须做mitm: return true
36
+ // 需要做mitm: return false
37
+ match: function(headers, host) {
38
+ host = trimHost(host);
39
+ // Hack,Youtube 广告拦截不兼容浏览器,浏览器访问 Youtube 的广告拦截都不做 mitm
40
+ if (!this.isYoutubeApp(this.getUA(headers)) &&
41
+ this.filtered_mitm_domains.some(domain => host.endsWith(domain) || host === domain)) {
42
+ return true;
43
+ } else {
44
+ return false;
45
+ }
46
+ }
47
+ };