block-proxy 0.1.12 → 0.1.14
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.
- package/.claude/settings.local.json +33 -1
- package/.claude/skills/build-client/skill.md +24 -0
- package/.claude/skills/release-client/skill.md +68 -0
- package/CLAUDE.md +69 -67
- package/Dockerfile +1 -1
- package/README.md +38 -24
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/static/css/main.3f317ce6.css +2 -0
- package/build/static/css/main.3f317ce6.css.map +1 -0
- package/build/static/js/{main.2247fb80.js → main.af1923ea.js} +3 -3
- package/build/static/js/main.af1923ea.js.map +1 -0
- package/client/app.py +312 -0
- package/client/build.sh +84 -0
- package/client/config.py +49 -0
- package/client/config_window.py +155 -0
- package/client/icons/app.icns +0 -0
- package/client/icons/app_example.png +0 -0
- package/client/icons/app_icon.png +0 -0
- package/client/icons/backup/app_example.png +0 -0
- package/client/icons/backup/christmas-sock_dark.png +0 -0
- package/client/icons/backup/christmas-sock_light.png +0 -0
- package/client/icons/backup/socks_on_G.png +0 -0
- package/client/icons/backup/socks_on_M.png +0 -0
- package/client/icons/christmas-sock_dark.png +0 -0
- package/client/icons/christmas-sock_light.png +0 -0
- package/client/icons/christmas-sock_light_bar.png +0 -0
- package/client/icons/socks_on_G.png +0 -0
- package/client/icons/socks_on_G_bar.png +0 -0
- package/client/icons/socks_on_M.png +0 -0
- package/client/icons/socks_on_M_bar.png +0 -0
- package/client/main.py +28 -0
- package/client/proxy_core.py +475 -0
- package/client/requirements.txt +3 -0
- package/client/scripts/download_xray.sh +30 -0
- package/client/setup.py +30 -0
- package/client/system_proxy.py +94 -0
- package/client/tests/__init__.py +0 -0
- package/client/tests/test_config.py +72 -0
- package/client/tests/test_system_proxy.py +69 -0
- package/client/watch-icons.js +31 -0
- package/config.json +4 -199
- package/docs/superpowers/plans/2026-05-27-blockproxyclient.md +1274 -0
- package/docs/superpowers/specs/2026-05-27-blockproxyclient-design.md +264 -0
- package/package.json +11 -5
- package/proxy/proxy.js +19 -34
- package/server/express.js +17 -1
- package/src/App.css +596 -276
- package/src/App.js +25 -32
- package/src/index.css +3 -4
- package/test/lib/mock-server.js +133 -0
- package/test/proxy-tests.js +708 -0
- package/test/run.js +330 -0
- package/build/static/css/main.8bfa3d5f.css +0 -2
- package/build/static/css/main.8bfa3d5f.css.map +0 -1
- package/build/static/js/main.2247fb80.js.map +0 -1
- package/hack-of-anyproxy/lib/requestHandler.js +0 -1060
- /package/build/static/js/{main.2247fb80.js.LICENSE.txt → main.af1923ea.js.LICENSE.txt} +0 -0
|
@@ -1,1060 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const http = require('http'),
|
|
4
|
-
https = require('https'),
|
|
5
|
-
net = require('net'),
|
|
6
|
-
url = require('url'),
|
|
7
|
-
zlib = require('zlib'),
|
|
8
|
-
color = require('colorful'),
|
|
9
|
-
Buffer = require('buffer').Buffer,
|
|
10
|
-
util = require('./util'),
|
|
11
|
-
Stream = require('stream'),
|
|
12
|
-
logUtil = require('./log'),
|
|
13
|
-
co = require('co'),
|
|
14
|
-
WebSocket = require('ws'),
|
|
15
|
-
HttpsServerMgr = require('./httpsServerMgr'),
|
|
16
|
-
brotliTorb = require('brotli'),
|
|
17
|
-
Readable = require('stream').Readable;
|
|
18
|
-
|
|
19
|
-
const requestErrorHandler = require('./requestErrorHandler');
|
|
20
|
-
|
|
21
|
-
// to fix issue with TLS cache, refer to: https://github.com/nodejs/node/issues/8368
|
|
22
|
-
https.globalAgent.maxCachedSessions = 0;
|
|
23
|
-
|
|
24
|
-
const DEFAULT_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb
|
|
25
|
-
|
|
26
|
-
// console.log(normalizeIP('::ffff:192.168.124.118')); // "192.168.124.118"
|
|
27
|
-
// console.log(normalizeIP('::FFFF:10.0.0.5')); // "10.0.0.5"
|
|
28
|
-
// console.log(normalizeIP('192.168.1.100')); // "192.168.1.100"
|
|
29
|
-
// console.log(normalizeIP('[::ffff:172.16.0.10]:3000')); // "172.16.0.10"
|
|
30
|
-
// console.log(normalizeIP('::1')); // "::1"
|
|
31
|
-
function normalizeIP(rawIP) {
|
|
32
|
-
if (typeof rawIP !== 'string') return rawIP;
|
|
33
|
-
|
|
34
|
-
// 处理 [::ffff:192.168.1.1]:8080 这类格式(来自 req.url 或 proxy)
|
|
35
|
-
let ip = rawIP;
|
|
36
|
-
if (ip.startsWith('[')) {
|
|
37
|
-
const match = ip.match(/^\[([^\]]+)\]/);
|
|
38
|
-
if (match) ip = match[1];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 移除 ::ffff: 前缀(忽略大小写)
|
|
42
|
-
return ip.replace(/^::ffff:/i, '');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function normalizeSocketKey(ip, port) {
|
|
46
|
-
return ip + ":" + port
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 判断是否命中 responseRules
|
|
50
|
-
function matchResponseRule(responseRules, userConfig) {
|
|
51
|
-
try {
|
|
52
|
-
var hostname = userConfig.requestOptions.hostname.split(":")[0].toLowerCase();
|
|
53
|
-
var pathname = userConfig.requestOptions.path; // 带query
|
|
54
|
-
var url = `${userConfig.protocol}://${hostname}${pathname}`
|
|
55
|
-
var matched = false;
|
|
56
|
-
for (const item of responseRules) {
|
|
57
|
-
if (hostname.endsWith(item["host"]) && new RegExp(item['regexp']).test(url)) {
|
|
58
|
-
matched = true;
|
|
59
|
-
break;
|
|
60
|
-
} else {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return matched;
|
|
65
|
-
} catch (e) {
|
|
66
|
-
console.log(e);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
class CommonReadableStream extends Readable {
|
|
72
|
-
constructor(config) {
|
|
73
|
-
super({
|
|
74
|
-
highWaterMark: DEFAULT_CHUNK_COLLECT_THRESHOLD * 5
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
_read(size) {
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/*
|
|
83
|
-
* get error response for exception scenarios
|
|
84
|
-
*/
|
|
85
|
-
const getErrorResponse = (error, fullUrl) => {
|
|
86
|
-
// default error response
|
|
87
|
-
const errorResponse = {
|
|
88
|
-
statusCode: 500,
|
|
89
|
-
header: {
|
|
90
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
91
|
-
'Proxy-Error': true,
|
|
92
|
-
'Proxy-Error-Message': error ? JSON.stringify(error) : 'null'
|
|
93
|
-
},
|
|
94
|
-
body: requestErrorHandler.getErrorContent(error, fullUrl)
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
return errorResponse;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* fetch remote response
|
|
102
|
-
*
|
|
103
|
-
* @param {string} protocol
|
|
104
|
-
* @param {object} options options of http.request
|
|
105
|
-
* @param {buffer} reqData request body
|
|
106
|
-
* @param {object} config
|
|
107
|
-
* @param {boolean} config.dangerouslyIgnoreUnauthorized
|
|
108
|
-
* @param {boolean} config.chunkSizeThreshold
|
|
109
|
-
* @returns
|
|
110
|
-
*/
|
|
111
|
-
function fetchRemoteResponse(protocol, options, reqData, config) {
|
|
112
|
-
reqData = reqData || '';
|
|
113
|
-
return new Promise((resolve, reject) => {
|
|
114
|
-
delete options.headers['content-length']; // will reset the content-length after rule
|
|
115
|
-
delete options.headers['Content-Length'];
|
|
116
|
-
delete options.headers['Transfer-Encoding'];
|
|
117
|
-
delete options.headers['transfer-encoding'];
|
|
118
|
-
|
|
119
|
-
if (config.dangerouslyIgnoreUnauthorized) {
|
|
120
|
-
options.rejectUnauthorized = false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!config.chunkSizeThreshold) {
|
|
124
|
-
throw new Error('chunkSizeThreshold is required');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
//send request
|
|
128
|
-
const proxyReq = (/https/i.test(protocol) ? https : http).request(options, (res) => {
|
|
129
|
-
res.headers = util.getHeaderFromRawHeaders(res.rawHeaders);
|
|
130
|
-
//deal response header
|
|
131
|
-
const statusCode = res.statusCode;
|
|
132
|
-
const resHeader = res.headers;
|
|
133
|
-
let resDataChunks = []; // array of data chunks or stream
|
|
134
|
-
const rawResChunks = []; // the original response chunks
|
|
135
|
-
let resDataStream = null;
|
|
136
|
-
let resSize = 0;
|
|
137
|
-
const finishCollecting = () => {
|
|
138
|
-
new Promise((fulfill, rejectParsing) => {
|
|
139
|
-
if (resDataStream) {
|
|
140
|
-
fulfill(resDataStream);
|
|
141
|
-
} else {
|
|
142
|
-
const serverResData = Buffer.concat(resDataChunks);
|
|
143
|
-
fulfill(serverResData);
|
|
144
|
-
// 性能考虑,处理解压缩的逻辑放在了 MITM 的 Response 的回调里面,只有
|
|
145
|
-
// mitm response 的时候才解压,其他情况一律原样透传
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
}).then((serverResData) => {
|
|
149
|
-
resolve({
|
|
150
|
-
statusCode,
|
|
151
|
-
header: resHeader,
|
|
152
|
-
body: serverResData,
|
|
153
|
-
rawBody: rawResChunks,
|
|
154
|
-
_res: res,
|
|
155
|
-
});
|
|
156
|
-
}).catch((e) => {
|
|
157
|
-
reject(e);
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
//deal response data
|
|
162
|
-
res.on('data', (chunk) => {
|
|
163
|
-
rawResChunks.push(chunk);
|
|
164
|
-
if (resDataStream) { // stream mode
|
|
165
|
-
resDataStream.push(chunk);
|
|
166
|
-
} else { // dataChunks
|
|
167
|
-
resSize += chunk.length;
|
|
168
|
-
resDataChunks.push(chunk);
|
|
169
|
-
|
|
170
|
-
// stop collecting, convert to stream mode
|
|
171
|
-
if (resSize >= config.chunkSizeThreshold) {
|
|
172
|
-
resDataStream = new CommonReadableStream();
|
|
173
|
-
while (resDataChunks.length) {
|
|
174
|
-
resDataStream.push(resDataChunks.shift());
|
|
175
|
-
}
|
|
176
|
-
resDataChunks = null;
|
|
177
|
-
finishCollecting();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
res.on('end', () => {
|
|
183
|
-
if (resDataStream) {
|
|
184
|
-
resDataStream.push(null); // indicate the stream is end
|
|
185
|
-
} else {
|
|
186
|
-
finishCollecting();
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
res.on('error', (error) => {
|
|
190
|
-
logUtil.printLog('error happend in response:' + error, logUtil.T_ERR);
|
|
191
|
-
reject(error);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
proxyReq.on('error', reject);
|
|
196
|
-
proxyReq.end(reqData);
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* get request info from the ws client, includes:
|
|
202
|
-
host
|
|
203
|
-
port
|
|
204
|
-
path
|
|
205
|
-
protocol ws/wss
|
|
206
|
-
|
|
207
|
-
@param @required wsClient the ws client of WebSocket
|
|
208
|
-
*
|
|
209
|
-
*/
|
|
210
|
-
function getWsReqInfo(wsReq) {
|
|
211
|
-
const headers = wsReq.headers || {};
|
|
212
|
-
const host = headers.host;
|
|
213
|
-
const hostName = host.split(':')[0];
|
|
214
|
-
const port = host.split(':')[1];
|
|
215
|
-
|
|
216
|
-
// TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出
|
|
217
|
-
const path = wsReq.url || '/';
|
|
218
|
-
|
|
219
|
-
const isEncript = wsReq.connection && wsReq.connection.encrypted;
|
|
220
|
-
/**
|
|
221
|
-
* construct the request headers based on original connection,
|
|
222
|
-
* but delete the `sec-websocket-*` headers as they are already consumed by AnyProxy
|
|
223
|
-
*/
|
|
224
|
-
const getNoWsHeaders = () => {
|
|
225
|
-
const originHeaders = Object.assign({}, headers);
|
|
226
|
-
const originHeaderKeys = Object.keys(originHeaders);
|
|
227
|
-
originHeaderKeys.forEach((key) => {
|
|
228
|
-
// if the key matchs 'sec-websocket', delete it
|
|
229
|
-
if (/sec-websocket/ig.test(key)) {
|
|
230
|
-
delete originHeaders[key];
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
delete originHeaders.connection;
|
|
235
|
-
delete originHeaders.upgrade;
|
|
236
|
-
return originHeaders;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
headers: headers, // the full headers of origin ws connection
|
|
242
|
-
noWsHeaders: getNoWsHeaders(),
|
|
243
|
-
hostName: hostName,
|
|
244
|
-
port: port,
|
|
245
|
-
path: path,
|
|
246
|
-
protocol: isEncript ? 'wss' : 'ws'
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// 只在 beforeSendRequest 时用,req 是 http 封装后的
|
|
251
|
-
function getSourceIp(req, socketMap) {
|
|
252
|
-
// 如果是 http 请求,则直接是原始 IP
|
|
253
|
-
// 如果是 https 请求,则是127.0.0.1
|
|
254
|
-
var localIp;
|
|
255
|
-
try {
|
|
256
|
-
if (req.client.remoteAddress === undefined) {
|
|
257
|
-
localIp = "255.255.255.254";
|
|
258
|
-
} else {
|
|
259
|
-
localIp = req.client.remoteAddress.split(":").pop();
|
|
260
|
-
}
|
|
261
|
-
} catch (e) {
|
|
262
|
-
console.log(e);
|
|
263
|
-
localIp = "255.255.255.254";
|
|
264
|
-
return localIp;
|
|
265
|
-
}
|
|
266
|
-
var connectionPort = getConnectionPort(req.socket.server._connectionKey);
|
|
267
|
-
if (localIp != '127.0.0.1' && localIp != '0.0.0.0') {
|
|
268
|
-
return localIp;
|
|
269
|
-
} else {
|
|
270
|
-
var mapKey = '127.0.0.1:' + connectionPort;
|
|
271
|
-
if (socketMap.has(mapKey)) {
|
|
272
|
-
// console.log(socketMap.get(mapKey).remoteAddress);
|
|
273
|
-
if (socketMap.get(mapKey).remoteAddress === undefined) {
|
|
274
|
-
localIp = "0.0.0.0";
|
|
275
|
-
} else {
|
|
276
|
-
localIp = socketMap.get(mapKey).remoteAddress.split(":").pop();
|
|
277
|
-
}
|
|
278
|
-
// try {
|
|
279
|
-
// localIp = socketMap.get(mapKey).remoteAddress.split(":").pop();
|
|
280
|
-
// } catch (e) {
|
|
281
|
-
// localIp = "127.0.0.1";
|
|
282
|
-
// }
|
|
283
|
-
}
|
|
284
|
-
return localIp;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// 6::::51479 -> '51479'
|
|
289
|
-
function getConnectionPort(connectionKey) {
|
|
290
|
-
return connectionKey.split(":").pop();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* get a request handler for http/https server
|
|
295
|
-
*
|
|
296
|
-
* @param {RequestHandler} reqHandlerCtx
|
|
297
|
-
* @param {object} userRule
|
|
298
|
-
* @param {Recorder} recorder
|
|
299
|
-
* @returns
|
|
300
|
-
*/
|
|
301
|
-
function getUserReqHandler(userRule, recorder) {
|
|
302
|
-
const reqHandlerCtx = this
|
|
303
|
-
|
|
304
|
-
return function (req, userRes) {
|
|
305
|
-
/*
|
|
306
|
-
note
|
|
307
|
-
req.url is wired
|
|
308
|
-
in http server: http://www.example.com/a/b/c
|
|
309
|
-
in https server: /a/b/c
|
|
310
|
-
*/
|
|
311
|
-
// console.log(req.socket.server._connectionKey);
|
|
312
|
-
// beforeSendRequest 前调用,这里有原始的 ip
|
|
313
|
-
// console.log(reqHandlerCtx.httpServerPort)
|
|
314
|
-
// console.log(reqHandlerCtx.cltSockets);
|
|
315
|
-
req.sourceIp = getSourceIp(req, reqHandlerCtx.cltSockets);
|
|
316
|
-
|
|
317
|
-
const host = req.headers.host;
|
|
318
|
-
const protocol = (!!req.connection.encrypted && !(/^http:/).test(req.url)) ? 'https' : 'http';
|
|
319
|
-
|
|
320
|
-
// try find fullurl https://github.com/alibaba/anyproxy/issues/419
|
|
321
|
-
let fullUrl = protocol + '://' + host + req.url;
|
|
322
|
-
if (protocol === 'http') {
|
|
323
|
-
const reqUrlPattern = url.parse(req.url);
|
|
324
|
-
if (reqUrlPattern.host && reqUrlPattern.protocol) {
|
|
325
|
-
fullUrl = req.url;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const urlPattern = url.parse(fullUrl);
|
|
330
|
-
const path = urlPattern.path;
|
|
331
|
-
const chunkSizeThreshold = DEFAULT_CHUNK_COLLECT_THRESHOLD;
|
|
332
|
-
|
|
333
|
-
let resourceInfo = null;
|
|
334
|
-
let resourceInfoId = -1;
|
|
335
|
-
let reqData;
|
|
336
|
-
let requestDetail;
|
|
337
|
-
|
|
338
|
-
// refer to https://github.com/alibaba/anyproxy/issues/103
|
|
339
|
-
// construct the original headers as the reqheaders
|
|
340
|
-
req.headers = util.getHeaderFromRawHeaders(req.rawHeaders);
|
|
341
|
-
|
|
342
|
-
logUtil.printLog(color.green(`received request to: ${req.method} ${host}${path}`));
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* fetch complete req data
|
|
346
|
-
*/
|
|
347
|
-
const fetchReqData = () => new Promise((resolve) => {
|
|
348
|
-
const postData = [];
|
|
349
|
-
req.on('data', (chunk) => {
|
|
350
|
-
postData.push(chunk);
|
|
351
|
-
});
|
|
352
|
-
req.on('end', () => {
|
|
353
|
-
reqData = Buffer.concat(postData);
|
|
354
|
-
resolve();
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* prepare detailed request info
|
|
360
|
-
*/
|
|
361
|
-
const prepareRequestDetail = () => {
|
|
362
|
-
const options = {
|
|
363
|
-
hostname: urlPattern.hostname || req.headers.host,
|
|
364
|
-
port: urlPattern.port || req.port || (/https/.test(protocol) ? 443 : 80),
|
|
365
|
-
path,
|
|
366
|
-
method: req.method,
|
|
367
|
-
headers: req.headers
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
requestDetail = {
|
|
371
|
-
requestOptions: options,
|
|
372
|
-
protocol,
|
|
373
|
-
url: fullUrl,
|
|
374
|
-
requestData: reqData,
|
|
375
|
-
_req: req
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
return Promise.resolve();
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* send response to client
|
|
383
|
-
*
|
|
384
|
-
* @param {object} finalResponseData
|
|
385
|
-
* @param {number} finalResponseData.statusCode
|
|
386
|
-
* @param {object} finalResponseData.header
|
|
387
|
-
* @param {buffer|string} finalResponseData.body
|
|
388
|
-
*/
|
|
389
|
-
const sendFinalResponse = (finalResponseData) => {
|
|
390
|
-
const responseInfo = finalResponseData.response;
|
|
391
|
-
const resHeader = responseInfo.header;
|
|
392
|
-
const responseBody = responseInfo.body || '';
|
|
393
|
-
|
|
394
|
-
const transferEncoding = resHeader['transfer-encoding'] || resHeader['Transfer-Encoding'] || '';
|
|
395
|
-
const contentLength = resHeader['content-length'] || resHeader['Content-Length'];
|
|
396
|
-
const connection = resHeader.Connection || resHeader.connection;
|
|
397
|
-
if (contentLength) {
|
|
398
|
-
// delete resHeader['content-length'];
|
|
399
|
-
// delete resHeader['Content-Length'];
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// set proxy-connection
|
|
403
|
-
if (connection) {
|
|
404
|
-
resHeader['x-anyproxy-origin-connection'] = connection;
|
|
405
|
-
// 如果是 407 响应,把 Connection: close 透传下去
|
|
406
|
-
// 如果是 200 响应,把 Connection: keep-alive 透传下去
|
|
407
|
-
if (responseInfo.statusCode === 407 || responseInfo.statusCode === 200) {
|
|
408
|
-
// do nothing
|
|
409
|
-
} else {
|
|
410
|
-
// modified at 2026-1-2 彻底把connection状态打开,原封不动的透传下去
|
|
411
|
-
// delete resHeader.connection;
|
|
412
|
-
// delete resHeader.Connection;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// resHeader['Connection'] = "keep-alive";
|
|
417
|
-
|
|
418
|
-
if (!responseInfo) {
|
|
419
|
-
throw new Error('failed to get response info');
|
|
420
|
-
} else if (!responseInfo.statusCode) {
|
|
421
|
-
throw new Error('failed to get response status code')
|
|
422
|
-
} else if (!responseInfo.header) {
|
|
423
|
-
throw new Error('filed to get response header');
|
|
424
|
-
}
|
|
425
|
-
// if there is no transfer-encoding, set the content-length
|
|
426
|
-
if (!global._throttle
|
|
427
|
-
&& transferEncoding !== 'chunked'
|
|
428
|
-
&& !(responseBody instanceof CommonReadableStream)
|
|
429
|
-
) {
|
|
430
|
-
resHeader['Content-Length'] = util.getByteSize(responseBody);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
userRes.writeHead(responseInfo.statusCode, resHeader);
|
|
434
|
-
|
|
435
|
-
if (global._throttle) {
|
|
436
|
-
if (responseBody instanceof CommonReadableStream) {
|
|
437
|
-
responseBody.pipe(global._throttle.throttle()).pipe(userRes);
|
|
438
|
-
} else {
|
|
439
|
-
const thrStream = new Stream();
|
|
440
|
-
thrStream.pipe(global._throttle.throttle()).pipe(userRes);
|
|
441
|
-
thrStream.emit('data', responseBody);
|
|
442
|
-
thrStream.emit('end');
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
if (responseBody instanceof CommonReadableStream) {
|
|
446
|
-
responseBody.pipe(userRes);
|
|
447
|
-
} else {
|
|
448
|
-
userRes.end(responseBody);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return responseInfo;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// fetch complete request data
|
|
456
|
-
co(fetchReqData)
|
|
457
|
-
.then(prepareRequestDetail)
|
|
458
|
-
|
|
459
|
-
.then(() => {
|
|
460
|
-
// record request info
|
|
461
|
-
if (recorder) {
|
|
462
|
-
resourceInfo = {
|
|
463
|
-
host,
|
|
464
|
-
method: req.method,
|
|
465
|
-
path,
|
|
466
|
-
protocol,
|
|
467
|
-
url: protocol + '://' + host + path,
|
|
468
|
-
req,
|
|
469
|
-
startTime: new Date().getTime()
|
|
470
|
-
};
|
|
471
|
-
resourceInfoId = recorder.appendRecord(resourceInfo);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
resourceInfo.reqBody = reqData.toString(); //TODO: deal reqBody in webInterface.js
|
|
476
|
-
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
|
|
477
|
-
} catch (e) { }
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
// invoke rule before sending request
|
|
481
|
-
.then(co.wrap(function *() {
|
|
482
|
-
const userModifiedInfo = (yield userRule.beforeSendRequest(Object.assign({}, requestDetail))) || {};
|
|
483
|
-
const finalReqDetail = {};
|
|
484
|
-
['protocol', 'requestOptions', 'requestData', 'response'].map((key) => {
|
|
485
|
-
finalReqDetail[key] = userModifiedInfo[key] || requestDetail[key]
|
|
486
|
-
});
|
|
487
|
-
return finalReqDetail;
|
|
488
|
-
}))
|
|
489
|
-
|
|
490
|
-
// route user config
|
|
491
|
-
.then(co.wrap(function *(userConfig) {
|
|
492
|
-
// Modified 2026-1-15
|
|
493
|
-
// 增加了对 responseRules 的判断,如果需要对 BeforeResponse 进行拦截
|
|
494
|
-
// 则让 chunkSizeThreshold 为很大的值(默认20M),以便让 response 一次性返回,方便BeforeResponse处理
|
|
495
|
-
// 否则以 512k 为上线,只要返回的数据达到阈值,就直接返回给调用端,以提高性能
|
|
496
|
-
if (userRule.responseRules && matchResponseRule(userRule.responseRules, userConfig)) {
|
|
497
|
-
var _chunkSizeThreshold = chunkSizeThreshold;
|
|
498
|
-
} else {
|
|
499
|
-
var _chunkSizeThreshold = 64 * 1024; // 64K
|
|
500
|
-
}
|
|
501
|
-
if (userConfig.response) {
|
|
502
|
-
// user-assigned local response
|
|
503
|
-
userConfig._directlyPassToRespond = true;
|
|
504
|
-
return userConfig;
|
|
505
|
-
} else if (userConfig.requestOptions) {
|
|
506
|
-
const remoteResponse = yield fetchRemoteResponse(userConfig.protocol, userConfig.requestOptions, userConfig.requestData, {
|
|
507
|
-
dangerouslyIgnoreUnauthorized: reqHandlerCtx.dangerouslyIgnoreUnauthorized,
|
|
508
|
-
chunkSizeThreshold: _chunkSizeThreshold,
|
|
509
|
-
});
|
|
510
|
-
return {
|
|
511
|
-
response: {
|
|
512
|
-
statusCode: remoteResponse.statusCode,
|
|
513
|
-
header: remoteResponse.header,
|
|
514
|
-
body: remoteResponse.body,
|
|
515
|
-
rawBody: remoteResponse.rawBody
|
|
516
|
-
},
|
|
517
|
-
_res: remoteResponse._res,
|
|
518
|
-
};
|
|
519
|
-
} else {
|
|
520
|
-
throw new Error('lost response or requestOptions, failed to continue');
|
|
521
|
-
}
|
|
522
|
-
}))
|
|
523
|
-
|
|
524
|
-
// invoke rule before responding to client
|
|
525
|
-
.then(co.wrap(function *(responseData) {
|
|
526
|
-
if (responseData._directlyPassToRespond) {
|
|
527
|
-
return responseData;
|
|
528
|
-
} else if (responseData.response.body && responseData.response.body instanceof CommonReadableStream) { // in stream mode
|
|
529
|
-
return responseData;
|
|
530
|
-
} else {
|
|
531
|
-
// TODO: err etimeout
|
|
532
|
-
return (yield userRule.beforeSendResponse(Object.assign({}, requestDetail), Object.assign({}, responseData))) || responseData;
|
|
533
|
-
}
|
|
534
|
-
}))
|
|
535
|
-
|
|
536
|
-
.catch(co.wrap(function *(error) {
|
|
537
|
-
logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR);
|
|
538
|
-
|
|
539
|
-
// jayli
|
|
540
|
-
// console.log('----------------------');
|
|
541
|
-
// console.log(error, /*requestDetail.requestOptions*/);
|
|
542
|
-
|
|
543
|
-
let errorResponse = getErrorResponse(error, fullUrl);
|
|
544
|
-
|
|
545
|
-
// call user rule
|
|
546
|
-
try {
|
|
547
|
-
const userResponse = yield userRule.onError(Object.assign({}, requestDetail), error);
|
|
548
|
-
if (userResponse && userResponse.response && userResponse.response.header) {
|
|
549
|
-
errorResponse = userResponse.response;
|
|
550
|
-
}
|
|
551
|
-
} catch (e) {}
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
response: errorResponse
|
|
555
|
-
};
|
|
556
|
-
}))
|
|
557
|
-
.then(sendFinalResponse)
|
|
558
|
-
|
|
559
|
-
//update record info
|
|
560
|
-
.then((responseInfo) => {
|
|
561
|
-
resourceInfo.endTime = new Date().getTime();
|
|
562
|
-
resourceInfo.res = { //construct a self-defined res object
|
|
563
|
-
statusCode: responseInfo.statusCode,
|
|
564
|
-
headers: responseInfo.header,
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
resourceInfo.statusCode = responseInfo.statusCode;
|
|
568
|
-
resourceInfo.resHeader = responseInfo.header;
|
|
569
|
-
resourceInfo.resBody = responseInfo.body instanceof CommonReadableStream ? '(big stream)' : (responseInfo.body || '');
|
|
570
|
-
resourceInfo.length = resourceInfo.resBody.length;
|
|
571
|
-
|
|
572
|
-
// console.info('===> resbody in record', resourceInfo);
|
|
573
|
-
|
|
574
|
-
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
|
|
575
|
-
})
|
|
576
|
-
.catch((e) => {
|
|
577
|
-
logUtil.printLog(color.green('Send final response failed:' + e.message), logUtil.T_ERR);
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* get a handler for CONNECT request
|
|
584
|
-
*
|
|
585
|
-
* @param {RequestHandler} reqHandlerCtx
|
|
586
|
-
* @param {object} userRule
|
|
587
|
-
* @param {Recorder} recorder
|
|
588
|
-
* @param {object} httpsServerMgr
|
|
589
|
-
* @returns
|
|
590
|
-
*/
|
|
591
|
-
function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
|
|
592
|
-
const reqHandlerCtx = this; reqHandlerCtx.conns = new Map(); reqHandlerCtx.cltSockets = new Map()
|
|
593
|
-
|
|
594
|
-
return function (req, cltSocket, head) {
|
|
595
|
-
// 只在 https 里调用
|
|
596
|
-
// console.log(cltSocket);
|
|
597
|
-
// console.log(reqHandlerCtx.conns);
|
|
598
|
-
// console.log(req.socket.remoteAddress);
|
|
599
|
-
const host = req.url.split(':')[0],
|
|
600
|
-
targetPort = req.url.split(':')[1];
|
|
601
|
-
let shouldIntercept;
|
|
602
|
-
let interceptWsRequest = false;
|
|
603
|
-
let requestDetail;
|
|
604
|
-
let resourceInfo = null;
|
|
605
|
-
let resourceInfoId = -1;
|
|
606
|
-
const requestStream = new CommonReadableStream();
|
|
607
|
-
|
|
608
|
-
/*
|
|
609
|
-
1. write HTTP/1.1 200 to client
|
|
610
|
-
2. get request data
|
|
611
|
-
3. tell if it is a websocket request
|
|
612
|
-
4.1 if (websocket || do_not_intercept) --> pipe to target server
|
|
613
|
-
4.2 else --> pipe to local server and do man-in-the-middle attack
|
|
614
|
-
*/
|
|
615
|
-
co(function *() {
|
|
616
|
-
// determine whether to use the man-in-the-middle server
|
|
617
|
-
logUtil.printLog(color.green('received https CONNECT request ' + host));
|
|
618
|
-
requestDetail = {
|
|
619
|
-
host: req.url,
|
|
620
|
-
_req: req
|
|
621
|
-
};
|
|
622
|
-
// the return value in default rule is null
|
|
623
|
-
// so if the value is null, will take it as final value
|
|
624
|
-
shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
|
|
625
|
-
|
|
626
|
-
// otherwise, will take the passed in option
|
|
627
|
-
if (shouldIntercept === null) {
|
|
628
|
-
shouldIntercept = reqHandlerCtx.forceProxyHttps;
|
|
629
|
-
}
|
|
630
|
-
})
|
|
631
|
-
.then(() => {
|
|
632
|
-
return new Promise((resolve, reject) => {
|
|
633
|
-
// 为客户端 socket 添加错误处理
|
|
634
|
-
cltSocket.on('error', (error) => {
|
|
635
|
-
// 检查是否是 EPIPE 错误
|
|
636
|
-
if (error.code === 'EPIPE') {
|
|
637
|
-
// Client closed the connection before we could write the response.
|
|
638
|
-
// This is usually not a critical error for the proxy itself.
|
|
639
|
-
logUtil.printLog(`Client prematurely closed connection (EPIPE) during CONNECT response for ${req.url}`, logUtil.T_DBG);
|
|
640
|
-
// Resolve the promise to continue the flow, as the connection is already broken.
|
|
641
|
-
// We can't write anything anymore.
|
|
642
|
-
resolve(); // Important: Resolve to avoid hanging
|
|
643
|
-
} else if (error.code === 'ECONNRESET') {
|
|
644
|
-
// Client reset the connection
|
|
645
|
-
logUtil.printLog(`Client reset connection (ECONNRESET) during CONNECT response for ${req.url}`, logUtil.T_DBG);
|
|
646
|
-
resolve(); // Resolve to avoid hanging
|
|
647
|
-
} else {
|
|
648
|
-
// Log other errors as errors and reject the promise
|
|
649
|
-
logUtil.printLog(`Socket error writing CONNECT response to client for ${req.url}: ${util.collectErrorLog(error)}`, logUtil.T_ERR);
|
|
650
|
-
// Optionally notify user rule if needed, though less common for client socket errors during initial response
|
|
651
|
-
// co.wrap(function *() {
|
|
652
|
-
// try {
|
|
653
|
-
// yield userRule.onClientSocketError(requestDetail, error);
|
|
654
|
-
// } catch (e) {
|
|
655
|
-
// logUtil.printLog(`Error notifying user rule about client socket error: ${e.message}`, logUtil.T_WARN);
|
|
656
|
-
// }
|
|
657
|
-
// })();
|
|
658
|
-
reject(error); // Reject for non-EPIPE errors if they should fail the flow
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// Attempt to write the response
|
|
663
|
-
try {
|
|
664
|
-
cltSocket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', (writeErr) => {
|
|
665
|
-
if (writeErr) {
|
|
666
|
-
// The write callback might also receive the error,
|
|
667
|
-
// but the 'error' event listener on the socket is the standard place.
|
|
668
|
-
// If writeErr is EPIPE, the socket 'error' event should have fired.
|
|
669
|
-
// We don't need special handling here if the socket listener is robust.
|
|
670
|
-
// However, if for some reason the error *doesn't* fire on the socket event,
|
|
671
|
-
// this callback might catch it. But relying on the socket 'error' event is better.
|
|
672
|
-
// For robustness, you *could* check here too, but it's often redundant.
|
|
673
|
-
// if (writeErr.code === 'EPIPE') {
|
|
674
|
-
// logUtil.printLog(`Write EPIPE for ${req.url}`, logUtil.T_DBG);
|
|
675
|
-
// resolve(); // Resolve if write failed due to EPIPE
|
|
676
|
-
// return;
|
|
677
|
-
// }
|
|
678
|
-
// If the socket 'error' event handles it, this callback might not need specific EPIPE logic.
|
|
679
|
-
// Standard callback handling:
|
|
680
|
-
if (writeErr) {
|
|
681
|
-
// This should ideally not happen if the socket 'error' listener is working,
|
|
682
|
-
// but as a last resort, handle it here.
|
|
683
|
-
if (writeErr.code === 'EPIPE' || writeErr.code === 'ECONNRESET') {
|
|
684
|
-
// If client closed before we finished writing, resolve silently (or with debug log)
|
|
685
|
-
logUtil.printLog(`Write failed due to client disconnect (EPIPE/ECONNRESET) for ${req.url}`, logUtil.T_DBG);
|
|
686
|
-
resolve(); // Resolve to allow the flow to continue (next steps might not be needed)
|
|
687
|
-
} else {
|
|
688
|
-
// For other write errors, reject
|
|
689
|
-
reject(writeErr);
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
// Write successful
|
|
693
|
-
resolve();
|
|
694
|
-
}
|
|
695
|
-
} else {
|
|
696
|
-
// Write successful
|
|
697
|
-
resolve();
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
} catch (syncErr) {
|
|
701
|
-
// This catch handles synchronous errors during write() call, which is unlikely for EPIPE
|
|
702
|
-
// but good practice.
|
|
703
|
-
logUtil.printLog(`Sync error during write for ${req.url}: ${util.collectErrorLog(syncErr)}`, logUtil.T_ERR);
|
|
704
|
-
reject(syncErr);
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
})
|
|
708
|
-
.then(() => {
|
|
709
|
-
return new Promise((resolve, reject) => {
|
|
710
|
-
let resolved = false;
|
|
711
|
-
cltSocket.on('data', (chunk) => {
|
|
712
|
-
requestStream.push(chunk);
|
|
713
|
-
if (!resolved) {
|
|
714
|
-
resolved = true;
|
|
715
|
-
try {
|
|
716
|
-
const chunkString = chunk.toString();
|
|
717
|
-
if (chunkString.indexOf('GET ') === 0) {
|
|
718
|
-
shouldIntercept = false; // websocket, do not intercept
|
|
719
|
-
|
|
720
|
-
// if there is '/do-not-proxy' in the request, do not intercept the websocket
|
|
721
|
-
// to avoid AnyProxy itself be proxied
|
|
722
|
-
if (reqHandlerCtx.wsIntercept && chunkString.indexOf('GET /do-not-proxy') !== 0) {
|
|
723
|
-
interceptWsRequest = true;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
} catch (e) {
|
|
727
|
-
console.error(e);
|
|
728
|
-
}
|
|
729
|
-
resolve();
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
cltSocket.on('error', (error) => {
|
|
733
|
-
// --- 改进版本 ---
|
|
734
|
-
if (error.code === 'EPIPE') {
|
|
735
|
-
// Client likely closed the connection before we could write the full response.
|
|
736
|
-
// This is usually not a problem with the proxy itself.
|
|
737
|
-
logUtil.printLog(`Client prematurely closed connection (EPIPE) for ${req.method} ${req.url}`, logUtil.T_DBG);
|
|
738
|
-
} else if (error.code == "ECONNRESET") {
|
|
739
|
-
logUtil.printLog(`ECONNRESET---`, logUtil.T_ERR);
|
|
740
|
-
} else {
|
|
741
|
-
// Log other socket errors as errors
|
|
742
|
-
logUtil.printLog(`Socket error for ${req.method} ${req.url}: ${util.collectErrorLog(error)}`, logUtil.T_ERR);
|
|
743
|
-
// Notify user rule about non-EPIPE errors
|
|
744
|
-
co.wrap(function *() {
|
|
745
|
-
try {
|
|
746
|
-
yield userRule.onClientSocketError(requestDetail, error);
|
|
747
|
-
} catch (e) {
|
|
748
|
-
logUtil.printLog(`Error notifying user rule about socket error: ${e.message}`, logUtil.T_WARN);
|
|
749
|
-
}
|
|
750
|
-
})();
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
cltSocket.on('end', () => {
|
|
754
|
-
requestStream.push(null);
|
|
755
|
-
});
|
|
756
|
-
});
|
|
757
|
-
})
|
|
758
|
-
.then((result) => {
|
|
759
|
-
// log and recorder
|
|
760
|
-
if (shouldIntercept) {
|
|
761
|
-
logUtil.printLog('will forward to local https server');
|
|
762
|
-
} else {
|
|
763
|
-
logUtil.printLog('will bypass the man-in-the-middle proxy');
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
//record
|
|
767
|
-
if (recorder) {
|
|
768
|
-
resourceInfo = {
|
|
769
|
-
host,
|
|
770
|
-
method: req.method,
|
|
771
|
-
path: '',
|
|
772
|
-
url: 'https://' + host,
|
|
773
|
-
req,
|
|
774
|
-
startTime: new Date().getTime()
|
|
775
|
-
};
|
|
776
|
-
resourceInfoId = recorder.appendRecord(resourceInfo);
|
|
777
|
-
}
|
|
778
|
-
})
|
|
779
|
-
.then(() => {
|
|
780
|
-
// determine the request target
|
|
781
|
-
if (!shouldIntercept) {
|
|
782
|
-
// server info from the original request
|
|
783
|
-
const originServer = {
|
|
784
|
-
host,
|
|
785
|
-
port: (targetPort === 80) ? 443 : targetPort
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const localHttpServer = {
|
|
789
|
-
host: 'localhost',
|
|
790
|
-
port: reqHandlerCtx.httpServerPort
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// for ws request, redirect them to local ws server
|
|
794
|
-
return interceptWsRequest ? localHttpServer : originServer;
|
|
795
|
-
} else {
|
|
796
|
-
return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port }));
|
|
797
|
-
}
|
|
798
|
-
})
|
|
799
|
-
.then((serverInfo) => {
|
|
800
|
-
if (!serverInfo.port || !serverInfo.host) {
|
|
801
|
-
throw new Error('failed to get https server info');
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
return new Promise((resolve, reject) => {
|
|
805
|
-
const conn = net.connect(serverInfo.port, serverInfo.host, () => {
|
|
806
|
-
//throttle for direct-foward https
|
|
807
|
-
if (global._throttle && !shouldIntercept) {
|
|
808
|
-
requestStream.pipe(conn);
|
|
809
|
-
conn.pipe(global._throttle.throttle()).pipe(cltSocket);
|
|
810
|
-
} else {
|
|
811
|
-
requestStream.pipe(conn);
|
|
812
|
-
conn.pipe(cltSocket);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
resolve();
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
conn.on('error', (e) => {
|
|
819
|
-
reject(e);
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
reqHandlerCtx.conns.set(serverInfo.host + ':' + serverInfo.port, conn)
|
|
823
|
-
reqHandlerCtx.cltSockets.set(serverInfo.host + ':' + serverInfo.port, cltSocket)
|
|
824
|
-
});
|
|
825
|
-
})
|
|
826
|
-
.then(() => {
|
|
827
|
-
if (recorder) {
|
|
828
|
-
resourceInfo.endTime = new Date().getTime();
|
|
829
|
-
resourceInfo.statusCode = '200';
|
|
830
|
-
resourceInfo.resHeader = {};
|
|
831
|
-
resourceInfo.resBody = '';
|
|
832
|
-
resourceInfo.length = 0;
|
|
833
|
-
|
|
834
|
-
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
|
|
835
|
-
}
|
|
836
|
-
})
|
|
837
|
-
.catch(co.wrap(function *(error) {
|
|
838
|
-
logUtil.printLog(util.collectErrorLog(error), logUtil.T_ERR);
|
|
839
|
-
|
|
840
|
-
try {
|
|
841
|
-
yield userRule.onConnectError(requestDetail, error);
|
|
842
|
-
} catch (e) { }
|
|
843
|
-
|
|
844
|
-
try {
|
|
845
|
-
let errorHeader = 'Proxy-Error: true\r\n';
|
|
846
|
-
errorHeader += 'Proxy-Error-Message: ' + (error || 'null') + '\r\n';
|
|
847
|
-
errorHeader += 'Content-Type: text/html\r\n';
|
|
848
|
-
cltSocket.write('HTTP/1.1 502\r\n' + errorHeader + '\r\n\r\n');
|
|
849
|
-
} catch (e) { }
|
|
850
|
-
}));
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* get a websocket event handler
|
|
856
|
-
@param @required {object} wsClient
|
|
857
|
-
*/
|
|
858
|
-
function getWsHandler(userRule, recorder, wsClient, wsReq) {
|
|
859
|
-
const self = this;
|
|
860
|
-
try {
|
|
861
|
-
let resourceInfoId = -1;
|
|
862
|
-
const resourceInfo = {
|
|
863
|
-
wsMessages: [] // all ws messages go through AnyProxy
|
|
864
|
-
};
|
|
865
|
-
const clientMsgQueue = [];
|
|
866
|
-
const serverInfo = getWsReqInfo(wsReq);
|
|
867
|
-
const serverInfoPort = serverInfo.port ? `:${serverInfo.port}` : '';
|
|
868
|
-
const wsUrl = `${serverInfo.protocol}://${serverInfo.hostName}${serverInfoPort}${serverInfo.path}`;
|
|
869
|
-
const proxyWs = new WebSocket(wsUrl, '', {
|
|
870
|
-
rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized,
|
|
871
|
-
headers: serverInfo.noWsHeaders
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
if (recorder) {
|
|
875
|
-
Object.assign(resourceInfo, {
|
|
876
|
-
host: serverInfo.hostName,
|
|
877
|
-
method: 'WebSocket',
|
|
878
|
-
path: serverInfo.path,
|
|
879
|
-
url: wsUrl,
|
|
880
|
-
req: wsReq,
|
|
881
|
-
startTime: new Date().getTime()
|
|
882
|
-
});
|
|
883
|
-
resourceInfoId = recorder.appendRecord(resourceInfo);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* store the messages before the proxy ws is ready
|
|
888
|
-
*/
|
|
889
|
-
const sendProxyMessage = (event) => {
|
|
890
|
-
const message = event.data;
|
|
891
|
-
if (proxyWs.readyState === 1) {
|
|
892
|
-
// if there still are msg queue consuming, keep it going
|
|
893
|
-
if (clientMsgQueue.length > 0) {
|
|
894
|
-
clientMsgQueue.push(message);
|
|
895
|
-
} else {
|
|
896
|
-
proxyWs.send(message);
|
|
897
|
-
}
|
|
898
|
-
} else {
|
|
899
|
-
clientMsgQueue.push(message);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* consume the message in queue when the proxy ws is not ready yet
|
|
905
|
-
* will handle them from the first one-by-one
|
|
906
|
-
*/
|
|
907
|
-
const consumeMsgQueue = () => {
|
|
908
|
-
while (clientMsgQueue.length > 0) {
|
|
909
|
-
const message = clientMsgQueue.shift();
|
|
910
|
-
proxyWs.send(message);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* When the source ws is closed, we need to close the target websocket.
|
|
916
|
-
* If the source ws is normally closed, that is, the code is reserved, we need to transfrom them
|
|
917
|
-
*/
|
|
918
|
-
const getCloseFromOriginEvent = (event) => {
|
|
919
|
-
const code = event.code || '';
|
|
920
|
-
const reason = event.reason || '';
|
|
921
|
-
let targetCode = '';
|
|
922
|
-
let targetReason = '';
|
|
923
|
-
if (code >= 1004 && code <= 1006) {
|
|
924
|
-
targetCode = 1000; // normal closure
|
|
925
|
-
targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`;
|
|
926
|
-
} else {
|
|
927
|
-
targetCode = code;
|
|
928
|
-
targetReason = reason;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
return {
|
|
932
|
-
code: targetCode,
|
|
933
|
-
reason: targetReason
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* consruct a message Record from message event
|
|
939
|
-
* @param @required {event} messageEvent the event from websockt.onmessage
|
|
940
|
-
* @param @required {boolean} isToServer whether the message is to or from server
|
|
941
|
-
*
|
|
942
|
-
*/
|
|
943
|
-
const recordMessage = (messageEvent, isToServer) => {
|
|
944
|
-
const message = {
|
|
945
|
-
time: Date.now(),
|
|
946
|
-
message: messageEvent.data,
|
|
947
|
-
isToServer: isToServer
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
// resourceInfo.wsMessages.push(message);
|
|
951
|
-
recorder && recorder.updateRecordWsMessage(resourceInfoId, message);
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
proxyWs.onopen = () => {
|
|
955
|
-
consumeMsgQueue();
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// this event is fired when the connection is build and headers is returned
|
|
959
|
-
proxyWs.on('upgrade', (response) => {
|
|
960
|
-
resourceInfo.endTime = new Date().getTime();
|
|
961
|
-
const headers = response.headers;
|
|
962
|
-
resourceInfo.res = { //construct a self-defined res object
|
|
963
|
-
statusCode: response.statusCode,
|
|
964
|
-
headers: headers,
|
|
965
|
-
};
|
|
966
|
-
|
|
967
|
-
resourceInfo.statusCode = response.statusCode;
|
|
968
|
-
resourceInfo.resHeader = headers;
|
|
969
|
-
resourceInfo.resBody = '';
|
|
970
|
-
resourceInfo.length = resourceInfo.resBody.length;
|
|
971
|
-
|
|
972
|
-
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
proxyWs.onerror = (e) => {
|
|
976
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
|
|
977
|
-
wsClient.close(1001, e.message);
|
|
978
|
-
proxyWs.close(1001);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
proxyWs.onmessage = (event) => {
|
|
982
|
-
recordMessage(event, false);
|
|
983
|
-
wsClient.readyState === 1 && wsClient.send(event.data);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
proxyWs.onclose = (event) => {
|
|
987
|
-
logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`);
|
|
988
|
-
const targetCloseInfo = getCloseFromOriginEvent(event);
|
|
989
|
-
wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason);
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
wsClient.onmessage = (event) => {
|
|
993
|
-
recordMessage(event, true);
|
|
994
|
-
sendProxyMessage(event);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
wsClient.onclose = (event) => {
|
|
998
|
-
logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`);
|
|
999
|
-
const targetCloseInfo = getCloseFromOriginEvent(event);
|
|
1000
|
-
proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason);
|
|
1001
|
-
}
|
|
1002
|
-
} catch (e) {
|
|
1003
|
-
logUtil.debug('WebSocket Proxy Error:' + e.message);
|
|
1004
|
-
logUtil.debug(e.stack);
|
|
1005
|
-
console.error(e);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
class RequestHandler {
|
|
1010
|
-
/**
|
|
1011
|
-
* Creates an instance of RequestHandler.
|
|
1012
|
-
*
|
|
1013
|
-
* @param {object} config
|
|
1014
|
-
* @param {boolean} config.forceProxyHttps proxy all https requests
|
|
1015
|
-
* @param {boolean} config.dangerouslyIgnoreUnauthorized
|
|
1016
|
-
@param {number} config.httpServerPort the http port AnyProxy do the proxy
|
|
1017
|
-
* @param {object} rule
|
|
1018
|
-
* @param {Recorder} recorder
|
|
1019
|
-
*
|
|
1020
|
-
* @memberOf RequestHandler
|
|
1021
|
-
*/
|
|
1022
|
-
constructor(config, rule, recorder) {
|
|
1023
|
-
const reqHandlerCtx = this;
|
|
1024
|
-
this.forceProxyHttps = false;
|
|
1025
|
-
this.dangerouslyIgnoreUnauthorized = false;
|
|
1026
|
-
this.httpServerPort = '';
|
|
1027
|
-
this.wsIntercept = false;
|
|
1028
|
-
|
|
1029
|
-
if (config.forceProxyHttps) {
|
|
1030
|
-
this.forceProxyHttps = true;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (config.dangerouslyIgnoreUnauthorized) {
|
|
1034
|
-
this.dangerouslyIgnoreUnauthorized = true;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (config.wsIntercept) {
|
|
1038
|
-
this.wsIntercept = config.wsIntercept;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
this.httpServerPort = config.httpServerPort;
|
|
1042
|
-
const default_rule = util.freshRequire('./rule_default');
|
|
1043
|
-
const userRule = util.merge(default_rule, rule);
|
|
1044
|
-
|
|
1045
|
-
reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]);
|
|
1046
|
-
reqHandlerCtx.wsHandler = getWsHandler.bind(this, userRule, recorder);
|
|
1047
|
-
reqHandlerCtx.httpServerPort = config.httpServerPort;
|
|
1048
|
-
|
|
1049
|
-
reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({
|
|
1050
|
-
handler: reqHandlerCtx.userRequestHandler,
|
|
1051
|
-
wsHandler: reqHandlerCtx.wsHandler, // websocket
|
|
1052
|
-
hostname: '127.0.0.1',
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
module.exports = RequestHandler;
|
|
1060
|
-
|