diff --git a/bin/anyproxy b/bin/anyproxy
index 330e083..108bc5d 100755
--- a/bin/anyproxy
+++ b/bin/anyproxy
@@ -1,6 +1,7 @@
#!/usr/bin/env node
-'use strict'
+'use strict';
+
const program = require('commander'),
color = require('colorful'),
packageInfo = require('../package.json'),
@@ -17,6 +18,7 @@ program
.option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists')
.option('-s, --silent', 'do not print anything into terminal')
.option('-c, --clear', 'clear all the certificates and temp files')
+ .option('--ws-intercept', 'intercept websocket')
.option('--ignore-unauthorized-ssl', 'ignore all ssl error')
.parse(process.argv);
@@ -62,7 +64,8 @@ if (program.clear) {
webInterface: {
enable: true,
webPort: program.web,
- },
+ },
+ wsIntercept: program.wsIntercept,
forceProxyHttps: program.intercept,
dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl,
silent: program.silent
@@ -107,7 +110,7 @@ if (program.clear) {
} catch (e) {}
logUtil.printLog(errorTipText, logUtil.T_ERR);
try {
- proxyServer && proxyServer.close();
+ proxyServer && proxyServer.close();
} catch (e) {}
process.exit();
});
diff --git a/docs-src/cn/src_doc.md b/docs-src/cn/src_doc.md
index 02b9ab0..ae01fdc 100644
--- a/docs-src/cn/src_doc.md
+++ b/docs-src/cn/src_doc.md
@@ -84,6 +84,7 @@ const options = {
},
throttle: 10000,
forceProxyHttps: false,
+ wsIntercept: false, // 不开启websocket代理
silent: false
};
const proxyServer = new AnyProxy.ProxyServer(options);
@@ -110,6 +111,7 @@ proxyServer.close();
* `forceProxyHttps` {boolean} 是否强制拦截所有的https,忽略规则模块的返回,默认`false`
* `silent` {boolean} 是否屏蔽所有console输出,默认`false`
* `dangerouslyIgnoreUnauthorized` {boolean} 是否忽略请求中的证书错误,默认`false`
+ * `wsIntercept` {boolean} 是否开启websocket代理,默认`false`
* `webInterface` {object} web版界面配置
* `enable` {boolean} 是否启用web版界面,默认`false`
* `webPort` {number} web版界面端口号,默认`8002`
diff --git a/docs-src/en/src_doc.md b/docs-src/en/src_doc.md
index da11810..36872ed 100644
--- a/docs-src/en/src_doc.md
+++ b/docs-src/en/src_doc.md
@@ -83,6 +83,7 @@ const options = {
},
throttle: 10000,
forceProxyHttps: false,
+ wsIntercept: false,
silent: false
};
const proxyServer = new AnyProxy.ProxyServer(options);
@@ -106,11 +107,12 @@ proxyServer.close();
* `port` {number} required, port number of proxy server
* `rule` {object} your rule module
* `throttle` {number} throttle in kb/s, unlimited for default
- * `forceProxyHttps` {boolean} in force intercept all https request, false for default
- * `silent` {boolean} if keep silent in console, false for default`false`
- * `dangerouslyIgnoreUnauthorized` {boolean} if ignore certificate error in request, false for default
+ * `forceProxyHttps` {boolean} in force intercept all https request, default to `false`
+ * `silent` {boolean} if keep silent in console, false for default `false`
+ * `dangerouslyIgnoreUnauthorized` {boolean} if ignore certificate error in request, default to `false`
+ * `wsIntercept` {boolean} whether to intercept websocket, default to `false`
* `webInterface` {object} config for web interface
- * `enable` {boolean} if enable web interface, false for default
+ * `enable` {boolean} if enable web interface, default to `false`
* `webPort` {number} port number for web interface
* Event: `ready`
* emit when proxy server is ready
diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js
index d57c14a..e66c840 100644
--- a/lib/httpsServerMgr.js
+++ b/lib/httpsServerMgr.js
@@ -9,12 +9,12 @@ const async = require('async'),
certMgr = require('./certMgr'),
logUtil = require('./log'),
util = require('./util'),
+ wsServerMgr = require('./wsServerMgr'),
co = require('co'),
constants = require('constants'),
asyncTask = require('async-task-mgr');
const createSecureContext = tls.createSecureContext || crypto.createSecureContext;
-
//using sni to avoid multiple ports
function SNIPrepareCert(serverName, SNICallback) {
let keyContent,
@@ -80,7 +80,6 @@ function createHttpsServer(config) {
key: keyContent,
cert: crtContent
}, config.handler).listen(config.port);
-
resolve(server);
});
});
@@ -131,6 +130,7 @@ class httpsServerMgr {
this.instanceDefaultHost = '127.0.0.1';
this.httpsAsyncTask = new asyncTask();
this.handler = config.handler;
+ this.wsHandler = config.wsHandler
}
getSharedHttpsServer(hostname) {
@@ -159,12 +159,15 @@ class httpsServerMgr {
});
}
-
- httpsServer.on('upgrade', (req, socket, head) => {
- const reqHost = req.headers.host || 'unknown host';
- 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);
- socket.end();
+ wsServerMgr.getWsServer({
+ server: httpsServer,
+ connHandler: self.wsHandler
});
+
+ httpsServer.on('upgrade', (req, cltSocket, head) => {
+ logUtil.debug('will let WebSocket server to handle the upgrade event');
+ });
+
const result = {
host: finalHost,
port: instancePort,
diff --git a/lib/log.js b/lib/log.js
index 470a494..18adb3e 100644
--- a/lib/log.js
+++ b/lib/log.js
@@ -25,6 +25,7 @@ function printLog(content, type) {
if (!ifPrint) {
return;
}
+
const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss');
switch (type) {
case LogLevelMap.tip: {
@@ -62,6 +63,7 @@ function printLog(content, type) {
}
case LogLevelMap.debug: {
+ console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
return;
}
@@ -73,6 +75,27 @@ function printLog(content, type) {
}
module.exports.printLog = printLog;
+
+module.exports.debug = (content) => {
+ printLog(content, LogLevelMap.debug);
+};
+
+module.exports.info = (content) => {
+ printLog(content, LogLevelMap.tip);
+};
+
+module.exports.warn = (content) => {
+ printLog(content, LogLevelMap.warn);
+};
+
+module.exports.error = (content) => {
+ printLog(content, LogLevelMap.error);
+};
+
+module.exports.ruleError = (content) => {
+ printLog(content, LogLevelMap.rule_error);
+};
+
module.exports.setPrintStatus = setPrintStatus;
module.exports.setLogLevel = setLogLevel;
module.exports.T_TIP = LogLevelMap.tip;
diff --git a/lib/recorder.js b/lib/recorder.js
index 4bb49c0..1a37739 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -4,11 +4,30 @@
const Datastore = require('nedb'),
path = require('path'),
fs = require('fs'),
+ logUtil = require('./log'),
events = require('events'),
iconv = require('iconv-lite'),
+ fastJson = require('fast-json-stringify'),
proxyUtil = require('./util');
+const wsMessageStingify = fastJson({
+ title: 'ws message stringify',
+ type: 'object',
+ properties: {
+ time: {
+ type: 'integer'
+ },
+ message: {
+ type: 'string'
+ },
+ isToServer: {
+ type: 'boolean'
+ }
+ }
+});
+
const BODY_FILE_PRFIX = 'res_body_';
+const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
const CACHE_DIR_PREFIX = 'cache_r';
function getCacheDir() {
const rand = Math.floor(Math.random() * 1000000),
@@ -85,6 +104,10 @@ class Recorder extends events.EventEmitter {
}
}
+ emitUpdateLatestWsMessage(id, message) {
+ this.emit('updateLatestWsMsg', message);
+ }
+
updateRecord(id, info) {
if (id < 0) return;
const self = this;
@@ -98,6 +121,28 @@ class Recorder extends events.EventEmitter {
self.emitUpdate(id, finalInfo);
}
+ /**
+ * This method shall be called at each time there are new message
+ *
+ */
+ updateRecordWsMessage(id, message) {
+ const cachePath = this.cachePath;
+ if (id < 0) return;
+ try {
+ const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
+
+ fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', () => {});
+ } catch (e) {
+ console.error(e);
+ logUtil.error(e.message + e.stack);
+ }
+
+ this.emitUpdateLatestWsMessage(id, {
+ id: id,
+ message: message
+ });
+ }
+
updateExtInfo(id, extInfo) {
const self = this;
const db = self.db;
@@ -138,6 +183,10 @@ class Recorder extends events.EventEmitter {
fs.writeFile(bodyFile, info.resBody, () => {});
}
+ /**
+ * get body and websocket file
+ *
+ */
getBody(id, cb) {
const self = this;
const cachePath = self.cachePath;
@@ -159,6 +208,7 @@ class Recorder extends events.EventEmitter {
getDecodedBody(id, cb) {
const self = this;
const result = {
+ method: '',
type: 'unknown',
mime: '',
content: ''
@@ -170,6 +220,9 @@ class Recorder extends events.EventEmitter {
return;
}
+ // also put the `method` back, so the client can decide whether to load ws messages
+ result.method = doc[0].method;
+
self.getBody(id, (error, bodyContent) => {
if (error) {
cb(error);
@@ -212,6 +265,44 @@ class Recorder extends events.EventEmitter {
});
}
+ /**
+ * get decoded WebSoket messages
+ *
+ */
+ getDecodedWsMessage(id, cb) {
+ const self = this;
+ const cachePath = self.cachePath;
+
+ if (id < 0) {
+ cb && cb([]);
+ }
+
+ const wsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
+ fs.access(wsMessageFile, fs.F_OK || fs.R_OK, (err) => {
+ if (err) {
+ cb && cb(err);
+ } else {
+ fs.readFile(wsMessageFile, 'utf8', (error, content) => {
+ if (error) {
+ cb && cb(err);
+ }
+
+ try {
+ // remove the last dash "," if it has, since it's redundant
+ // and also add brackets to make it a complete JSON structure
+ content = `[${content.replace(/,$/, '')}]`;
+ const messages = JSON.parse(content);
+ cb(null, messages);
+ } catch (e) {
+ console.error(e);
+ logUtil.error(e.message + e.stack);
+ cb(e);
+ }
+ });
+ }
+ });
+ }
+
getSingleRecord(id, cb) {
const self = this;
const db = self.db;
diff --git a/lib/requestHandler.js b/lib/requestHandler.js
index 1ed7e97..9757fd6 100644
--- a/lib/requestHandler.js
+++ b/lib/requestHandler.js
@@ -11,6 +11,7 @@ const http = require('http'),
Stream = require('stream'),
logUtil = require('./log'),
co = require('co'),
+ WebSocket = require('ws'),
HttpsServerMgr = require('./httpsServerMgr'),
brotliTorb = require('brotli'),
Readable = require('stream').Readable;
@@ -201,6 +202,34 @@ function fetchRemoteResponse(protocol, options, reqData, config) {
});
}
+/**
+* get request info from the ws client, includes:
+ host
+ port
+ path
+ protocol ws/wss
+
+ @param @required wsClient the ws client of WebSocket
+*
+*/
+function getWsReqInfo(wsClient) {
+ const upgradeReq = wsClient.upgradeReq || {};
+ const header = upgradeReq.headers || {};
+ const host = header.host;
+ const hostName = host.split(':')[0];
+ const port = host.split(':')[1];
+
+ // TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出
+ const path = upgradeReq.url || '/';
+
+ const isEncript = true && upgradeReq.connection && upgradeReq.connection.encrypted;
+ return {
+ hostName: hostName,
+ port: port,
+ path: path,
+ protocol: isEncript ? 'wss' : 'ws'
+ };
+}
/**
* get a request handler for http/https server
*
@@ -471,10 +500,10 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
const host = req.url.split(':')[0],
targetPort = req.url.split(':')[1];
let shouldIntercept;
+ let interceptWsRequest = false;
let requestDetail;
let resourceInfo = null;
let resourceInfoId = -1;
-
const requestStream = new CommonReadableStream();
/*
@@ -487,14 +516,17 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
co(function *() {
// determine whether to use the man-in-the-middle server
logUtil.printLog(color.green('received https CONNECT request ' + host));
- if (reqHandlerCtx.forceProxyHttps) {
- shouldIntercept = true;
- } else {
- requestDetail = {
- host: req.url,
- _req: req
- };
- shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
+ requestDetail = {
+ host: req.url,
+ _req: req
+ };
+ // the return value in default rule is null
+ // so if the value is null, will take it as final value
+ shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
+
+ // otherwise, will take the passed in option
+ if (shouldIntercept === null) {
+ shouldIntercept = reqHandlerCtx.forceProxyHttps;
}
})
.then(() =>
@@ -513,7 +545,13 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
try {
const chunkString = chunk.toString();
if (chunkString.indexOf('GET ') === 0) {
- shouldIntercept = false; //websocket
+ shouldIntercept = false; // websocket, do not intercept
+
+ // if there is '/do-not-proxy' in the request, do not intercept the websocket
+ // to avoid AnyProxy itself be proxied
+ if (reqHandlerCtx.wsIntercept && chunkString.indexOf('GET /do-not-proxy') !== 0) {
+ interceptWsRequest = true;
+ }
}
} catch (e) {
console.error(e);
@@ -550,10 +588,19 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
.then(() => {
// determine the request target
if (!shouldIntercept) {
- return {
+ // server info from the original request
+ const originServer = {
host,
- port: (targetPort === 80) ? 443 : targetPort,
+ port: (targetPort === 80) ? 443 : targetPort
}
+
+ const localHttpServer = {
+ host: 'localhost',
+ port: reqHandlerCtx.httpServerPort
+ }
+
+ // for ws request, redirect them to local ws server
+ return interceptWsRequest ? localHttpServer : originServer;
} else {
return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port }));
}
@@ -613,6 +660,158 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
}
}
+/**
+* get a websocket event handler
+ @param @required {object} wsClient
+*/
+function getWsHandler(userRule, recorder, wsClient) {
+ const self = this;
+ try {
+ let resourceInfoId = -1;
+ const resourceInfo = {
+ wsMessages: [] // all ws messages go through AnyProxy
+ };
+ const clientMsgQueue = [];
+ const serverInfo = getWsReqInfo(wsClient);
+ const wsUrl = `${serverInfo.protocol}://${serverInfo.hostName}:${serverInfo.port}${serverInfo.path}`;
+ const proxyWs = new WebSocket(wsUrl, '', {
+ rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized
+ });
+
+ if (recorder) {
+ Object.assign(resourceInfo, {
+ host: serverInfo.hostName,
+ method: 'WebSocket',
+ path: serverInfo.path,
+ url: wsUrl,
+ req: wsClient.upgradeReq || {},
+ startTime: new Date().getTime()
+ });
+ resourceInfoId = recorder.appendRecord(resourceInfo);
+ }
+
+ /**
+ * store the messages before the proxy ws is ready
+ */
+ const sendProxyMessage = (event) => {
+ const message = event.data;
+ if (proxyWs.readyState === 1) {
+ // if there still are msg queue consuming, keep it going
+ if (clientMsgQueue.length > 0) {
+ clientMsgQueue.push(message);
+ } else {
+ proxyWs.send(message);
+ }
+ } else {
+ clientMsgQueue.push(message);
+ }
+ }
+
+ /**
+ * consume the message in queue when the proxy ws is not ready yet
+ * will handle them from the first one-by-one
+ */
+ const consumeMsgQueue = () => {
+ while (clientMsgQueue.length > 0) {
+ const message = clientMsgQueue.shift();
+ proxyWs.send(message);
+ }
+ }
+
+ /**
+ * When the source ws is closed, we need to close the target websocket.
+ * If the source ws is normally closed, that is, the code is reserved, we need to transfrom them
+ */
+ const getCloseFromOriginEvent = (event) => {
+ const code = event.code || '';
+ const reason = event.reason || '';
+ let targetCode = '';
+ let targetReason = '';
+ if (code >= 1004 && code <= 1006) {
+ targetCode = 1000; // normal closure
+ targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`;
+ } else {
+ targetCode = code;
+ targetReason = reason;
+ }
+
+ return {
+ code: targetCode,
+ reason: targetReason
+ }
+ }
+
+ /**
+ * consruct a message Record from message event
+ * @param @required {event} messageEvent the event from websockt.onmessage
+ * @param @required {boolean} isToServer whether the message is to or from server
+ *
+ */
+ const recordMessage = (messageEvent, isToServer) => {
+ const message = {
+ time: Date.now(),
+ message: messageEvent.data,
+ isToServer: isToServer
+ };
+
+ // resourceInfo.wsMessages.push(message);
+ recorder && recorder.updateRecordWsMessage(resourceInfoId, message);
+ };
+
+ proxyWs.onopen = () => {
+ consumeMsgQueue();
+ }
+
+ // this event is fired when the connection is build and headers is returned
+ proxyWs.on('headers', (headers, response) => {
+ resourceInfo.endTime = new Date().getTime();
+ resourceInfo.res = { //construct a self-defined res object
+ statusCode: response.statusCode,
+ headers: headers,
+ };
+
+ resourceInfo.statusCode = response.statusCode;
+ resourceInfo.resHeader = headers;
+ resourceInfo.resBody = '';
+ resourceInfo.length = resourceInfo.resBody.length;
+
+ recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
+ });
+
+ proxyWs.onerror = (e) => {
+ // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
+ wsClient.close(1001, e.message);
+ proxyWs.close(1001);
+ }
+
+ proxyWs.onmessage = (event) => {
+ recordMessage(event, false);
+ wsClient.readyState === 1 && wsClient.send(event.data);
+ }
+
+ proxyWs.onclose = (event) => {
+ logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`);
+ const targetCloseInfo = getCloseFromOriginEvent(event);
+ wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason);
+ }
+
+ wsClient.onmessage = (event) => {
+ recordMessage(event, true);
+ sendProxyMessage(event);
+ }
+
+ wsClient.onclose = (event) => {
+ logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`);
+ const targetCloseInfo = getCloseFromOriginEvent(event);
+ proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason);
+ }
+ } catch (e) {
+ logUtil.debug('WebSocket Proxy Error:' + e.message);
+ logUtil.debug(e.stack);
+ console.error(e);
+ }
+}
+
class RequestHandler {
/**
@@ -621,6 +820,7 @@ class RequestHandler {
* @param {object} config
* @param {boolean} config.forceProxyHttps proxy all https requests
* @param {boolean} config.dangerouslyIgnoreUnauthorized
+ @param {number} config.httpServerPort the http port AnyProxy do the proxy
* @param {object} rule
* @param {Recorder} recorder
*
@@ -628,22 +828,36 @@ class RequestHandler {
*/
constructor(config, rule, recorder) {
const reqHandlerCtx = this;
+ this.forceProxyHttps = false;
+ this.dangerouslyIgnoreUnauthorized = false;
+ this.httpServerPort = '';
+ this.wsIntercept = false;
+
if (config.forceProxyHttps) {
this.forceProxyHttps = true;
}
+
if (config.dangerouslyIgnoreUnauthorized) {
this.dangerouslyIgnoreUnauthorized = true;
}
+
+ if (config.wsIntercept) {
+ this.wsIntercept = config.wsIntercept;
+ }
+
+ this.httpServerPort = config.httpServerPort;
const default_rule = util.freshRequire('./rule_default');
const userRule = util.merge(default_rule, rule);
reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]);
+ reqHandlerCtx.wsHandler = getWsHandler.bind(this, userRule, recorder);
reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({
- handler: reqHandlerCtx.userRequestHandler
+ handler: reqHandlerCtx.userRequestHandler,
+ wsHandler: reqHandlerCtx.wsHandler // websocket
});
- this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr])
+ this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]);
}
}
diff --git a/lib/rule_default.js b/lib/rule_default.js
index e5c441e..dc74910 100644
--- a/lib/rule_default.js
+++ b/lib/rule_default.js
@@ -1,12 +1,12 @@
'use strict';
module.exports = {
-
+
summary: 'the default rule for AnyProxy',
-
+
/**
- *
- *
+ *
+ *
* @param {object} requestDetail
* @param {string} requestDetail.protocol
* @param {object} requestDetail.requestOptions
@@ -23,8 +23,8 @@ module.exports = {
/**
- *
- *
+ *
+ *
* @param {object} requestDetail
* @param {object} responseDetail
*/
@@ -34,21 +34,22 @@ module.exports = {
/**
- *
- *
- * @param {any} requestDetail
- * @returns
+ * default to return null
+ * the user MUST return a boolean when they do implement the interface in rule
+ *
+ * @param {any} requestDetail
+ * @returns
*/
*beforeDealHttpsRequest(requestDetail) {
- return false;
+ return null;
},
/**
- *
- *
- * @param {any} requestDetail
- * @param {any} error
- * @returns
+ *
+ *
+ * @param {any} requestDetail
+ * @param {any} error
+ * @returns
*/
*onError(requestDetail, error) {
return null;
@@ -56,11 +57,11 @@ module.exports = {
/**
- *
- *
- * @param {any} requestDetail
- * @param {any} error
- * @returns
+ *
+ *
+ * @param {any} requestDetail
+ * @param {any} error
+ * @returns
*/
*onConnectError(requestDetail, error) {
return null;
diff --git a/lib/util.js b/lib/util.js
index f101c39..a09e924 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -4,8 +4,8 @@ const fs = require('fs'),
path = require('path'),
mime = require('mime-types'),
color = require('colorful'),
+ crypto = require('crypto'),
Buffer = require('buffer').Buffer,
- configUtil = require('./configUtil'),
logUtil = require('./log');
const networkInterfaces = require('os').networkInterfaces();
@@ -35,7 +35,7 @@ function getUserHome() {
module.exports.getUserHome = getUserHome;
function getAnyProxyHome() {
- const home = configUtil.getAnyProxyHome();
+ const home = path.join(getUserHome(), '/.anyproxy/');
if (!fs.existsSync(home)) {
fs.mkdirSync(home);
}
@@ -306,3 +306,18 @@ module.exports.isIpDomain = function (domain) {
return ipReg.test(domain);
};
+
+/**
+* To generic a Sec-WebSocket-Accept value
+* 1. append the `Sec-WebSocket-Key` request header with `matic string`
+* 2. get sha1 hash of the string
+* 3. get base64 of the sha1 hash
+*/
+module.exports.genericWsSecAccept = function (wsSecKey) {
+ // the string to generate the Sec-WebSocket-Accept
+ const magicString = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
+ const targetString = `${wsSecKey}${magicString}`;
+ const shasum = crypto.createHash('sha1');
+ shasum.update(targetString);
+ return shasum.digest('base64');
+}
diff --git a/lib/webInterface.js b/lib/webInterface.js
index 4ee90a3..6c04b32 100644
--- a/lib/webInterface.js
+++ b/lib/webInterface.js
@@ -128,6 +128,7 @@ class webInterface extends events.EventEmitter {
res.json({
id: query.id,
type: result.type,
+ method: result.meethod,
fileName: result.fileName,
ref: `/downloadBody?id=${query.id}&download=${isDownload}&raw=${!isDownload}`
});
@@ -143,6 +144,7 @@ class webInterface extends events.EventEmitter {
res.json({
id: query.id,
type: result.type,
+ method: result.method,
resBody: result.content
});
};
@@ -188,6 +190,22 @@ class webInterface extends events.EventEmitter {
}
});
+ app.get('/fetchWsMessages', (req, res) => {
+ const query = req.query;
+ if (query && query.id) {
+ recorder.getDecodedWsMessage(query.id, (err, messages) => {
+ if (err) {
+ console.error(err);
+ res.json([]);
+ return;
+ }
+ res.json(messages);
+ });
+ } else {
+ res.json([]);
+ }
+ });
+
app.get('/fetchCrtFile', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const _crtFilePath = certMgr.getRootCAFilePath();
diff --git a/lib/wsServer.js b/lib/wsServer.js
index e1fa39c..6fe69c5 100644
--- a/lib/wsServer.js
+++ b/lib/wsServer.js
@@ -99,7 +99,11 @@ class wsServer {
wss.broadcast = function (data) {
if (typeof data === 'object') {
- data = JSON.stringify(data);
+ try {
+ data = JSON.stringify(data);
+ } catch (e) {
+ console.error('==> errorr when do broadcast ', e, data);
+ }
}
for (const client of wss.clients) {
try {
@@ -137,6 +141,20 @@ class wsServer {
}
});
+ recorder.on('updateLatestWsMsg', (data) => {
+ try {
+ // console.info('==> update latestMsg ', data);
+ wss && wss.broadcast({
+ type: 'updateLatestWsMsg',
+ content: data
+ });
+ } catch (e) {
+ logUtil.error(e.message);
+ logUtil.error(e.stack);
+ console.error(e);
+ }
+ });
+
self.wss = wss;
});
}
diff --git a/lib/wsServerMgr.js b/lib/wsServerMgr.js
new file mode 100644
index 0000000..62e5a50
--- /dev/null
+++ b/lib/wsServerMgr.js
@@ -0,0 +1,39 @@
+/**
+* manage the websocket server
+*
+*/
+const ws = require('ws');
+const logUtil = require('./log.js');
+
+const WsServer = ws.Server;
+
+/**
+* get a new websocket server based on the server
+* @param @required {object} config
+ {string} config.server
+ {handler} config.handler
+*/
+function getWsServer(config) {
+ const wss = new WsServer({
+ server: config.server
+ });
+
+ wss.on('connection', config.connHandler);
+
+ wss.on('headers', (headers) => {
+ headers.push('x-anyproxy-websocket:true');
+ });
+
+ wss.on('error', e => {
+ logUtil.error(`error in websocket proxy: ${e.message},\r\n ${e.stack}`);
+ console.error('error happened in proxy websocket:', e)
+ });
+
+ wss.on('close', e => {
+ console.error('==> closing the ws server');
+ });
+
+ return wss;
+}
+
+module.exports.getWsServer = getWsServer;
diff --git a/package.json b/package.json
index 0e089ca..3eb4250 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"compression": "^1.4.4",
"es6-promise": "^3.3.1",
"express": "^4.8.5",
+ "fast-json-stringify": "^0.17.0",
"iconv-lite": "^0.4.6",
"inquirer": "^3.0.1",
"ip": "^0.3.2",
diff --git a/proxy.js b/proxy.js
index bf495d5..b907c76 100644
--- a/proxy.js
+++ b/proxy.js
@@ -11,6 +11,7 @@ const http = require('http'),
events = require('events'),
co = require('co'),
WebInterface = require('./lib/webInterface'),
+ wsServerMgr = require('./lib/wsServerMgr'),
ThrottleGroup = require('stream-throttle').ThrottleGroup;
// const memwatch = require('memwatch-next');
@@ -60,6 +61,7 @@ class ProxyCore extends events.EventEmitter {
* @param {boolean} [config.silent=false] - if keep the console silent
* @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response
* @param {object} [config.recorder] - recorder to use
+ * @param {boolean} [config.wsIntercept] - whether intercept websocket
*
* @memberOf ProxyCore
*/
@@ -114,6 +116,8 @@ class ProxyCore extends events.EventEmitter {
// init request handler
const RequestHandler = util.freshRequire('./requestHandler');
this.requestHandler = new RequestHandler({
+ wsIntercept: config.wsIntercept,
+ httpServerPort: config.port, // the http server port for http proxy
forceProxyHttps: !!config.forceProxyHttps,
dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized
}, this.proxyRule, this.recorder);
@@ -185,6 +189,10 @@ class ProxyCore extends events.EventEmitter {
},
function (callback) {
+ wsServerMgr.getWsServer({
+ server: self.httpProxyServer,
+ connHandler: self.requestHandler.wsHandler
+ });
// remember all sockets, so we can destory them when call the method 'close';
self.httpProxyServer.on('connection', (socket) => {
self.handleExistConnections.call(self, socket);
@@ -324,17 +332,10 @@ class ProxyServer extends ProxyCore {
this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, this.recorder);
}
- new Promise((resolve) => {
- // start web server
- if (this.webServerInstance) {
- resolve(this.webServerInstance.start());
- } else {
- resolve(null);
- }
- })
- .then(() => {
+ // start web server
+ this.webServerInstance.start().then(() => {
// start proxy core
- super.start()
+ super.start();
})
.catch((e) => {
this.emit('error', e);
diff --git a/test/server/server.js b/test/server/server.js
index d674633..cdb33e7 100644
--- a/test/server/server.js
+++ b/test/server/server.js
@@ -310,7 +310,7 @@ KoaServer.prototype.start = function () {
});
});
- wss.on('error', e => console.error('erro happened in wss:%s', error));
+ wss.on('error', e => console.error('error happened in wss:%s', e));
self.httpsServer.listen(HTTPS_PORT);
diff --git a/test/spec_rule/no_rule_websocket_spec.js b/test/spec_rule/no_rule_websocket_spec.js
index 94bc999..7feae15 100644
--- a/test/spec_rule/no_rule_websocket_spec.js
+++ b/test/spec_rule/no_rule_websocket_spec.js
@@ -6,23 +6,32 @@
const ProxyServerUtil = require('../util/ProxyServerUtil.js');
const { generateWsUrl, directWs, proxyWs } = require('../util/HttpUtil.js');
const Server = require('../server/server.js');
-const { printLog } = require('../util/CommonUtil.js');
+const { printLog, isArrayEqual } = require('../util/CommonUtil.js');
testWebsocket('ws');
testWebsocket('wss');
+testWebsocket('ws', true);
+testWebsocket('wss', true);
-function testWebsocket(protocol) {
+function testWebsocket(protocol, masked = false) {
describe('Test WebSocket in protocol : ' + protocol, () => {
const url = generateWsUrl(protocol, '/test/socket');
let serverInstance;
let proxyServer;
+ // the message to
+ const testMessageArray = [
+ 'Send the message with default option1',
+ 'Send the message with default option2',
+ 'Send the message with default option3',
+ 'Send the message with default option4'
+ ];
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
printLog('Start server for no_rule_websocket_spec');
serverInstance = new Server();
- proxyServer = ProxyServerUtil.proxyServerWithoutHttpsIntercept();
+ proxyServer = ProxyServerUtil.defaultProxyServer();
setTimeout(() => {
done();
@@ -36,32 +45,57 @@ function testWebsocket(protocol) {
});
it('Default websocket option', done => {
- const sendMessage = 'Send the message with default option';
- let directMessage; // set the flag for direct message, compare when both direct and proxy got message
- let proxyMessage;
+ const directMessages = []; // set the flag for direct message, compare when both direct and proxy got message
+ const proxyMessages = [];
+ let directHeaders;
+ let proxyHeaders;
const ws = directWs(url);
- const porxyWsRef = proxyWs(url);
+ const proxyWsRef = proxyWs(url);
ws.on('open', () => {
- ws.send(sendMessage);
+ ws.send(testMessageArray[0], masked);
+ for (let i = 1; i < testMessageArray.length; i++) {
+ setTimeout(() => {
+ ws.send(testMessageArray[i], masked);
+ }, 1000);
+ }
});
- porxyWsRef.on('open', () => {
- porxyWsRef.send(sendMessage);
+ proxyWsRef.on('open', () => {
+ try {
+ proxyWsRef.send(testMessageArray[0], masked);
+ for (let i = 1; i < testMessageArray.length; i++) {
+ setTimeout(() => {
+ proxyWsRef.send(testMessageArray[i], masked);
+ }, 1000);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ });
+
+ ws.on('headers', (headers) => {
+ directHeaders = headers;
+ compareMessageIfReady();
+ });
+
+ proxyWsRef.on('headers', (headers) => {
+ proxyHeaders = headers;
+ compareMessageIfReady();
});
ws.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
- directMessage = message.content;
+ directMessages.push(message.content);
compareMessageIfReady();
}
});
- porxyWsRef.on('message', (data, flag) => {
+ proxyWsRef.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
- proxyMessage = message.content;
+ proxyMessages.push(message.content);
compareMessageIfReady();
}
});
@@ -71,70 +105,24 @@ function testWebsocket(protocol) {
done.fail('Error happened in direct websocket');
});
- porxyWsRef.on('error', error => {
+ proxyWsRef.on('error', error => {
console.error('error happened in proxy websocket:', error);
done.fail('Error happened in proxy websocket');
});
function compareMessageIfReady() {
- if (directMessage && proxyMessage) {
- expect(directMessage).toEqual(proxyMessage);
- expect(directMessage).toEqual(sendMessage);
- done();
- }
- }
- });
-
- it('masked:true', done => {
- const sendMessage = 'Send the message with option masked:true';
- let directMessage; // set the flag for direct message, compare when both direct and proxy got message
- let proxyMessage;
-
- const ws = directWs(url);
- const porxyWsRef = proxyWs(url);
- ws.on('open', () => {
- ws.send(sendMessage, { masked: true });
- });
-
- porxyWsRef.on('open', () => {
- porxyWsRef.send(sendMessage, { masked: true });
- });
-
- ws.on('message', (data, flag) => {
- const message = JSON.parse(data);
- if (message.type === 'onMessage') {
- directMessage = message.content;
- compareMessageIfReady();
- }
- });
-
- porxyWsRef.on('message', (data, flag) => {
- const message = JSON.parse(data);
- if (message.type === 'onMessage') {
- proxyMessage = message.content;
- compareMessageIfReady();
- }
- });
-
- ws.on('error', error => {
- console.error('error happened in direct websocket:', error);
- done.fail('Error happened in direct websocket');
- });
-
- porxyWsRef.on('error', error => {
- console.error('error happened in proxy websocket:', error);
-
- done.fail('Error happened in proxy websocket');
- });
-
- function compareMessageIfReady() {
- if (directMessage && proxyMessage) {
- expect(directMessage).toEqual(proxyMessage);
- expect(directMessage).toEqual(sendMessage);
+ const targetLen = testMessageArray.length;
+ if (directMessages.length === targetLen
+ && proxyMessages.length === targetLen
+ && directHeaders && proxyHeaders
+ ) {
+ expect(isArrayEqual(directMessages, testMessageArray)).toBe(true);
+ expect(isArrayEqual(directMessages, proxyMessages)).toBe(true);
+ expect(directHeaders['x-anyproxy-websocket']).toBeUndefined();
+ expect(proxyHeaders['x-anyproxy-websocket']).toBe('true');
done();
}
}
});
});
}
-
diff --git a/test/util/CommonUtil.js b/test/util/CommonUtil.js
index 1d20e7e..8ba1f94 100644
--- a/test/util/CommonUtil.js
+++ b/test/util/CommonUtil.js
@@ -264,5 +264,6 @@ module.exports = {
printHilite,
isCommonReqEqual,
parseUrlQuery,
- stringSimilarity
+ stringSimilarity,
+ isArrayEqual: _isDeepEqual
};
diff --git a/test/util/ProxyServerUtil.js b/test/util/ProxyServerUtil.js
index e3ce885..1974040 100644
--- a/test/util/ProxyServerUtil.js
+++ b/test/util/ProxyServerUtil.js
@@ -13,6 +13,7 @@ const DEFAULT_OPTIONS = {
webPort: 8002, // optional, port for web interface
wsPort: 8003, // optional, internal port for web socket
},
+ wsIntercept: true,
throttle: 10000, // optional, speed limit in kb/s
forceProxyHttps: true, // intercept https as well
dangerouslyIgnoreUnauthorized: true,
diff --git a/web/src/common/WsUtil.js b/web/src/common/WsUtil.js
index 1aeec99..95835ba 100644
--- a/web/src/common/WsUtil.js
+++ b/web/src/common/WsUtil.js
@@ -4,12 +4,21 @@
*/
import { message } from 'antd';
-export function initWs(wsPort = 8003, key = '') {
+/**
+* Initiate a ws connection.
+* The default pay `do-not-proxy` means the ws do not need to be proxied.
+* This is very important for AnyProxy its' own server, such as WEB UI, and the
+* websocket detail panel, to prevent a recursive proxy.
+* @param {wsPort} wsPort the port of websocket
+* @param {key} path the path of the ws url
+*
+*/
+export function initWs(wsPort = 8003, path = 'do-not-proxy') {
if(!WebSocket){
throw (new Error('WebSocket is not supportted on this browser'));
}
- const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${key}`);
+ const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${path}`);
wsClient.onerror = (error) => {
console.error(error);
@@ -30,4 +39,3 @@ export function initWs(wsPort = 8003, key = '') {
export default {
initWs: initWs
};
-
diff --git a/web/src/common/commonUtil.js b/web/src/common/commonUtil.js
index 812a5eb..5a3f360 100644
--- a/web/src/common/commonUtil.js
+++ b/web/src/common/commonUtil.js
@@ -11,10 +11,11 @@ export function formatDate(date, formatter) {
if (typeof date !== 'object') {
date = new Date(date);
}
+
const transform = function(value) {
return value < 10 ? '0' + value : value;
};
- return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, function(match) {
+ return formatter.replace(/^YYYY|MM|DD|hh|mm|ss|ms/g, function(match) {
switch (match) {
case 'YYYY':
return transform(date.getFullYear());
@@ -28,6 +29,8 @@ export function formatDate(date, formatter) {
return transform(date.getHours());
case 'ss':
return transform(date.getSeconds());
+ case 'ms':
+ return transform(date.getMilliseconds());
}
});
}
diff --git a/web/src/component/record-detail.jsx b/web/src/component/record-detail.jsx
index 698a336..a3c4c9a 100644
--- a/web/src/component/record-detail.jsx
+++ b/web/src/component/record-detail.jsx
@@ -5,15 +5,12 @@
import React, { PropTypes } from 'react';
import ClassBind from 'classnames/bind';
-import { Menu, Table, notification, Spin } from 'antd';
-import clipboard from 'clipboard-js'
-import JsonViewer from 'component/json-viewer';
+import { Menu, Spin } from 'antd';
import ModalPanel from 'component/modal-panel';
import RecordRequestDetail from 'component/record-request-detail';
import RecordResponseDetail from 'component/record-response-detail';
+import RecordWsMessageDetail from 'component/record-ws-message-detail';
import { hideRecordDetail } from 'action/recordAction';
-import { selectText } from 'common/CommonUtil';
-import { curlify } from 'common/curlUtil';
import Style from './record-detail.less';
import CommonStyle from '../style/common.less';
@@ -21,7 +18,8 @@ import CommonStyle from '../style/common.less';
const StyleBind = ClassBind.bind(Style);
const PageIndexMap = {
REQUEST_INDEX: 'REQUEST_INDEX',
- RESPONSE_INDEX: 'RESPONSE_INDEX'
+ RESPONSE_INDEX: 'RESPONSE_INDEX',
+ WEBSOCKET_INDEX: 'WEBSOCKET_INDEX'
};
// the maximum length of the request body to decide whether to offer a download link for the request body
@@ -54,6 +52,10 @@ class RecordDetail extends React.Component {
});
}
+ hasWebSocket (recordDetail = {}) {
+ return recordDetail && recordDetail.method && recordDetail.method.toLowerCase() === 'websocket';
+ }
+
getRequestDiv(recordDetail) {
return