Skip to content

Commit 60bc149

Browse files
committed
add basic support for websocket proxy
1 parent e54fb23 commit 60bc149

24 files changed

+801
-142
lines changed

bin/anyproxy

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

3-
'use strict'
3+
'use strict';
4+
45
const program = require('commander'),
56
color = require('colorful'),
67
packageInfo = require('../package.json'),
@@ -17,6 +18,7 @@ program
1718
.option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists')
1819
.option('-s, --silent', 'do not print anything into terminal')
1920
.option('-c, --clear', 'clear all the certificates and temp files')
21+
.option('--ws-intercept', 'intercept websocket')
2022
.option('--ignore-unauthorized-ssl', 'ignore all ssl error')
2123
.parse(process.argv);
2224

@@ -62,7 +64,8 @@ if (program.clear) {
6264
webInterface: {
6365
enable: true,
6466
webPort: program.web,
65-
},
67+
},
68+
wsIntercept: program.wsIntercept,
6669
forceProxyHttps: program.intercept,
6770
dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl,
6871
silent: program.silent
@@ -107,7 +110,7 @@ if (program.clear) {
107110
} catch (e) {}
108111
logUtil.printLog(errorTipText, logUtil.T_ERR);
109112
try {
110-
proxyServer && proxyServer.close();
113+
proxyServer && proxyServer.close();
111114
} catch (e) {}
112115
process.exit();
113116
});

lib/httpsServerMgr.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ const async = require('async'),
99
certMgr = require('./certMgr'),
1010
logUtil = require('./log'),
1111
util = require('./util'),
12+
wsServerMgr = require('./wsServerMgr'),
1213
co = require('co'),
1314
constants = require('constants'),
1415
asyncTask = require('async-task-mgr');
1516

1617
const createSecureContext = tls.createSecureContext || crypto.createSecureContext;
17-
1818
//using sni to avoid multiple ports
1919
function SNIPrepareCert(serverName, SNICallback) {
2020
let keyContent,
@@ -80,7 +80,6 @@ function createHttpsServer(config) {
8080
key: keyContent,
8181
cert: crtContent
8282
}, config.handler).listen(config.port);
83-
8483
resolve(server);
8584
});
8685
});
@@ -131,6 +130,7 @@ class httpsServerMgr {
131130
this.instanceDefaultHost = '127.0.0.1';
132131
this.httpsAsyncTask = new asyncTask();
133132
this.handler = config.handler;
133+
this.wsHandler = config.wsHandler
134134
}
135135

136136
getSharedHttpsServer(hostname) {
@@ -159,12 +159,15 @@ class httpsServerMgr {
159159
});
160160
}
161161

162+
wsServerMgr.getWsServer({
163+
server: httpsServer,
164+
connHandler: self.wsHandler
165+
});
162166

163-
httpsServer.on('upgrade', (req, socket, head) => {
164-
const reqHost = req.headers.host || 'unknown host';
165-
logUtil.printLog(`wss:// is not supported when intercepting https. This request will be closed by AnyProxy. You may either exclude this domain in your rule file, or stop all https intercepting. (${reqHost})`, logUtil.T_ERR);
166-
socket.end();
167+
httpsServer.on('upgrade', (req, cltSocket, head) => {
168+
logUtil.debug('will let WebSocket server to handle the upgrade event');
167169
});
170+
168171
const result = {
169172
host: finalHost,
170173
port: instancePort,

lib/log.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function printLog(content, type) {
2525
if (!ifPrint) {
2626
return;
2727
}
28+
2829
const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss');
2930
switch (type) {
3031
case LogLevelMap.tip: {
@@ -62,6 +63,7 @@ function printLog(content, type) {
6263
}
6364

6465
case LogLevelMap.debug: {
66+
console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
6567
return;
6668
}
6769

@@ -73,6 +75,27 @@ function printLog(content, type) {
7375
}
7476

7577
module.exports.printLog = printLog;
78+
79+
module.exports.debug = (content) => {
80+
printLog(content, LogLevelMap.debug);
81+
};
82+
83+
module.exports.info = (content) => {
84+
printLog(content, LogLevelMap.tip);
85+
};
86+
87+
module.exports.warn = (content) => {
88+
printLog(content, LogLevelMap.warn);
89+
};
90+
91+
module.exports.error = (content) => {
92+
printLog(content, LogLevelMap.error);
93+
};
94+
95+
module.exports.ruleError = (content) => {
96+
printLog(content, LogLevelMap.rule_error);
97+
};
98+
7699
module.exports.setPrintStatus = setPrintStatus;
77100
module.exports.setLogLevel = setLogLevel;
78101
module.exports.T_TIP = LogLevelMap.tip;

lib/recorder.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
const Datastore = require('nedb'),
55
path = require('path'),
66
fs = require('fs'),
7+
logUtil = require('./log'),
78
events = require('events'),
89
iconv = require('iconv-lite'),
910
proxyUtil = require('./util');
1011

1112
const BODY_FILE_PRFIX = 'res_body_';
13+
const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
1214
const CACHE_DIR_PREFIX = 'cache_r';
1315
function getCacheDir() {
1416
const rand = Math.floor(Math.random() * 1000000),
@@ -85,6 +87,10 @@ class Recorder extends events.EventEmitter {
8587
}
8688
}
8789

90+
emitUpdateLatestWsMessage(id, message) {
91+
this.emit('updateLatestWsMsg', message);
92+
}
93+
8894
updateRecord(id, info) {
8995
if (id < 0) return;
9096
const self = this;
@@ -98,6 +104,27 @@ class Recorder extends events.EventEmitter {
98104
self.emitUpdate(id, finalInfo);
99105
}
100106

107+
/**
108+
* This method shall be called at each time there are new message
109+
*
110+
*/
111+
updateRecordWsMessage(id, message) {
112+
const cachePath = this.cachePath;
113+
if (id < 0) return;
114+
try {
115+
const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
116+
fs.appendFile(recordWsMessageFile, JSON.stringify(message) + ',', () => {});
117+
} catch (e) {
118+
console.error(e);
119+
logUtil.error(e.message + e.stack);
120+
}
121+
122+
this.emitUpdateLatestWsMessage(id, {
123+
id: id,
124+
message: message
125+
});
126+
}
127+
101128
updateExtInfo(id, extInfo) {
102129
const self = this;
103130
const db = self.db;
@@ -138,6 +165,10 @@ class Recorder extends events.EventEmitter {
138165
fs.writeFile(bodyFile, info.resBody, () => {});
139166
}
140167

168+
/**
169+
* get body and websocket file
170+
*
171+
*/
141172
getBody(id, cb) {
142173
const self = this;
143174
const cachePath = self.cachePath;
@@ -159,6 +190,7 @@ class Recorder extends events.EventEmitter {
159190
getDecodedBody(id, cb) {
160191
const self = this;
161192
const result = {
193+
method: '',
162194
type: 'unknown',
163195
mime: '',
164196
content: ''
@@ -170,6 +202,9 @@ class Recorder extends events.EventEmitter {
170202
return;
171203
}
172204

205+
// also put the `method` back, so the client can decide whether to load ws messages
206+
result.method = doc[0].method;
207+
173208
self.getBody(id, (error, bodyContent) => {
174209
if (error) {
175210
cb(error);
@@ -212,6 +247,44 @@ class Recorder extends events.EventEmitter {
212247
});
213248
}
214249

250+
/**
251+
* get decoded WebSoket messages
252+
*
253+
*/
254+
getDecodedWsMessage(id, cb) {
255+
const self = this;
256+
const cachePath = self.cachePath;
257+
258+
if (id < 0) {
259+
cb && cb([]);
260+
}
261+
262+
const wsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
263+
fs.access(wsMessageFile, fs.F_OK || fs.R_OK, (err) => {
264+
if (err) {
265+
cb && cb(err);
266+
} else {
267+
fs.readFile(wsMessageFile, 'utf8', (error, content) => {
268+
if (error) {
269+
cb && cb(err);
270+
}
271+
272+
try {
273+
// remove the last dash "," if it has, since it's redundant
274+
// and also add brackets to make it a complete JSON structure
275+
content = `[${content.replace(/,$/, '')}]`;
276+
const messages = JSON.parse(content);
277+
cb(null, messages);
278+
} catch (e) {
279+
console.error(e);
280+
logUtil.error(e.message + e.stack);
281+
cb(e);
282+
}
283+
});
284+
}
285+
});
286+
}
287+
215288
getSingleRecord(id, cb) {
216289
const self = this;
217290
const db = self.db;

0 commit comments

Comments
 (0)