Merge pull request #324 from alibaba/ws-proxy

add ws and wss support to AnyProxy
This commit is contained in:
Nick 2018-01-28 12:58:00 +08:00 committed by GitHub
commit b2fc71c3b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 835 additions and 153 deletions

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict' 'use strict';
const program = require('commander'), const program = require('commander'),
color = require('colorful'), color = require('colorful'),
packageInfo = require('../package.json'), packageInfo = require('../package.json'),
@ -17,6 +18,7 @@ program
.option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists') .option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists')
.option('-s, --silent', 'do not print anything into terminal') .option('-s, --silent', 'do not print anything into terminal')
.option('-c, --clear', 'clear all the certificates and temp files') .option('-c, --clear', 'clear all the certificates and temp files')
.option('--ws-intercept', 'intercept websocket')
.option('--ignore-unauthorized-ssl', 'ignore all ssl error') .option('--ignore-unauthorized-ssl', 'ignore all ssl error')
.parse(process.argv); .parse(process.argv);
@ -62,7 +64,8 @@ if (program.clear) {
webInterface: { webInterface: {
enable: true, enable: true,
webPort: program.web, webPort: program.web,
}, },
wsIntercept: program.wsIntercept,
forceProxyHttps: program.intercept, forceProxyHttps: program.intercept,
dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl, dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl,
silent: program.silent silent: program.silent
@ -107,7 +110,7 @@ if (program.clear) {
} catch (e) {} } catch (e) {}
logUtil.printLog(errorTipText, logUtil.T_ERR); logUtil.printLog(errorTipText, logUtil.T_ERR);
try { try {
proxyServer && proxyServer.close(); proxyServer && proxyServer.close();
} catch (e) {} } catch (e) {}
process.exit(); process.exit();
}); });

View File

@ -84,6 +84,7 @@ const options = {
}, },
throttle: 10000, throttle: 10000,
forceProxyHttps: false, forceProxyHttps: false,
wsIntercept: false, // 不开启websocket代理
silent: false silent: false
}; };
const proxyServer = new AnyProxy.ProxyServer(options); const proxyServer = new AnyProxy.ProxyServer(options);
@ -110,6 +111,7 @@ proxyServer.close();
* `forceProxyHttps` {boolean} 是否强制拦截所有的https忽略规则模块的返回默认`false` * `forceProxyHttps` {boolean} 是否强制拦截所有的https忽略规则模块的返回默认`false`
* `silent` {boolean} 是否屏蔽所有console输出默认`false` * `silent` {boolean} 是否屏蔽所有console输出默认`false`
* `dangerouslyIgnoreUnauthorized` {boolean} 是否忽略请求中的证书错误,默认`false` * `dangerouslyIgnoreUnauthorized` {boolean} 是否忽略请求中的证书错误,默认`false`
* `wsIntercept` {boolean} 是否开启websocket代理默认`false`
* `webInterface` {object} web版界面配置 * `webInterface` {object} web版界面配置
* `enable` {boolean} 是否启用web版界面默认`false` * `enable` {boolean} 是否启用web版界面默认`false`
* `webPort` {number} web版界面端口号默认`8002` * `webPort` {number} web版界面端口号默认`8002`

View File

@ -83,6 +83,7 @@ const options = {
}, },
throttle: 10000, throttle: 10000,
forceProxyHttps: false, forceProxyHttps: false,
wsIntercept: false,
silent: false silent: false
}; };
const proxyServer = new AnyProxy.ProxyServer(options); const proxyServer = new AnyProxy.ProxyServer(options);
@ -106,11 +107,12 @@ proxyServer.close();
* `port` {number} required, port number of proxy server * `port` {number} required, port number of proxy server
* `rule` {object} your rule module * `rule` {object} your rule module
* `throttle` {number} throttle in kb/s, unlimited for default * `throttle` {number} throttle in kb/s, unlimited for default
* `forceProxyHttps` {boolean} in force intercept all https 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` * `silent` {boolean} if keep silent in console, false for default `false`
* `dangerouslyIgnoreUnauthorized` {boolean} if ignore certificate error in request, false for default * `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 * `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 * `webPort` {number} port number for web interface
* Event: `ready` * Event: `ready`
* emit when proxy server is ready * emit when proxy server is ready

View File

@ -9,12 +9,12 @@ const async = require('async'),
certMgr = require('./certMgr'), certMgr = require('./certMgr'),
logUtil = require('./log'), logUtil = require('./log'),
util = require('./util'), util = require('./util'),
wsServerMgr = require('./wsServerMgr'),
co = require('co'), co = require('co'),
constants = require('constants'), constants = require('constants'),
asyncTask = require('async-task-mgr'); asyncTask = require('async-task-mgr');
const createSecureContext = tls.createSecureContext || crypto.createSecureContext; const createSecureContext = tls.createSecureContext || crypto.createSecureContext;
//using sni to avoid multiple ports //using sni to avoid multiple ports
function SNIPrepareCert(serverName, SNICallback) { function SNIPrepareCert(serverName, SNICallback) {
let keyContent, let keyContent,
@ -80,7 +80,6 @@ function createHttpsServer(config) {
key: keyContent, key: keyContent,
cert: crtContent cert: crtContent
}, config.handler).listen(config.port); }, config.handler).listen(config.port);
resolve(server); resolve(server);
}); });
}); });
@ -131,6 +130,7 @@ class httpsServerMgr {
this.instanceDefaultHost = '127.0.0.1'; this.instanceDefaultHost = '127.0.0.1';
this.httpsAsyncTask = new asyncTask(); this.httpsAsyncTask = new asyncTask();
this.handler = config.handler; this.handler = config.handler;
this.wsHandler = config.wsHandler
} }
getSharedHttpsServer(hostname) { getSharedHttpsServer(hostname) {
@ -159,12 +159,15 @@ class httpsServerMgr {
}); });
} }
wsServerMgr.getWsServer({
httpsServer.on('upgrade', (req, socket, head) => { server: httpsServer,
const reqHost = req.headers.host || 'unknown host'; connHandler: self.wsHandler
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();
}); });
httpsServer.on('upgrade', (req, cltSocket, head) => {
logUtil.debug('will let WebSocket server to handle the upgrade event');
});
const result = { const result = {
host: finalHost, host: finalHost,
port: instancePort, port: instancePort,

View File

@ -25,6 +25,7 @@ function printLog(content, type) {
if (!ifPrint) { if (!ifPrint) {
return; return;
} }
const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'); const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss');
switch (type) { switch (type) {
case LogLevelMap.tip: { case LogLevelMap.tip: {
@ -62,6 +63,7 @@ function printLog(content, type) {
} }
case LogLevelMap.debug: { case LogLevelMap.debug: {
console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
return; return;
} }
@ -73,6 +75,27 @@ function printLog(content, type) {
} }
module.exports.printLog = printLog; 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.setPrintStatus = setPrintStatus;
module.exports.setLogLevel = setLogLevel; module.exports.setLogLevel = setLogLevel;
module.exports.T_TIP = LogLevelMap.tip; module.exports.T_TIP = LogLevelMap.tip;

View File

@ -4,11 +4,30 @@
const Datastore = require('nedb'), const Datastore = require('nedb'),
path = require('path'), path = require('path'),
fs = require('fs'), fs = require('fs'),
logUtil = require('./log'),
events = require('events'), events = require('events'),
iconv = require('iconv-lite'), iconv = require('iconv-lite'),
fastJson = require('fast-json-stringify'),
proxyUtil = require('./util'); 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 BODY_FILE_PRFIX = 'res_body_';
const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
const CACHE_DIR_PREFIX = 'cache_r'; const CACHE_DIR_PREFIX = 'cache_r';
function getCacheDir() { function getCacheDir() {
const rand = Math.floor(Math.random() * 1000000), 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) { updateRecord(id, info) {
if (id < 0) return; if (id < 0) return;
const self = this; const self = this;
@ -98,6 +121,28 @@ class Recorder extends events.EventEmitter {
self.emitUpdate(id, finalInfo); 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) { updateExtInfo(id, extInfo) {
const self = this; const self = this;
const db = self.db; const db = self.db;
@ -138,6 +183,10 @@ class Recorder extends events.EventEmitter {
fs.writeFile(bodyFile, info.resBody, () => {}); fs.writeFile(bodyFile, info.resBody, () => {});
} }
/**
* get body and websocket file
*
*/
getBody(id, cb) { getBody(id, cb) {
const self = this; const self = this;
const cachePath = self.cachePath; const cachePath = self.cachePath;
@ -159,6 +208,7 @@ class Recorder extends events.EventEmitter {
getDecodedBody(id, cb) { getDecodedBody(id, cb) {
const self = this; const self = this;
const result = { const result = {
method: '',
type: 'unknown', type: 'unknown',
mime: '', mime: '',
content: '' content: ''
@ -170,6 +220,9 @@ class Recorder extends events.EventEmitter {
return; 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) => { self.getBody(id, (error, bodyContent) => {
if (error) { if (error) {
cb(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) { getSingleRecord(id, cb) {
const self = this; const self = this;
const db = self.db; const db = self.db;

View File

@ -11,6 +11,7 @@ const http = require('http'),
Stream = require('stream'), Stream = require('stream'),
logUtil = require('./log'), logUtil = require('./log'),
co = require('co'), co = require('co'),
WebSocket = require('ws'),
HttpsServerMgr = require('./httpsServerMgr'), HttpsServerMgr = require('./httpsServerMgr'),
brotliTorb = require('brotli'), brotliTorb = require('brotli'),
Readable = require('stream').Readable; 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 * get a request handler for http/https server
* *
@ -471,10 +500,10 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
const host = req.url.split(':')[0], const host = req.url.split(':')[0],
targetPort = req.url.split(':')[1]; targetPort = req.url.split(':')[1];
let shouldIntercept; let shouldIntercept;
let interceptWsRequest = false;
let requestDetail; let requestDetail;
let resourceInfo = null; let resourceInfo = null;
let resourceInfoId = -1; let resourceInfoId = -1;
const requestStream = new CommonReadableStream(); const requestStream = new CommonReadableStream();
/* /*
@ -487,14 +516,17 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
co(function *() { co(function *() {
// determine whether to use the man-in-the-middle server // determine whether to use the man-in-the-middle server
logUtil.printLog(color.green('received https CONNECT request ' + host)); logUtil.printLog(color.green('received https CONNECT request ' + host));
if (reqHandlerCtx.forceProxyHttps) { requestDetail = {
shouldIntercept = true; host: req.url,
} else { _req: req
requestDetail = { };
host: req.url, // the return value in default rule is null
_req: req // so if the value is null, will take it as final value
}; shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
// otherwise, will take the passed in option
if (shouldIntercept === null) {
shouldIntercept = reqHandlerCtx.forceProxyHttps;
} }
}) })
.then(() => .then(() =>
@ -513,7 +545,13 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
try { try {
const chunkString = chunk.toString(); const chunkString = chunk.toString();
if (chunkString.indexOf('GET ') === 0) { 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) { } catch (e) {
console.error(e); console.error(e);
@ -550,10 +588,19 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
.then(() => { .then(() => {
// determine the request target // determine the request target
if (!shouldIntercept) { if (!shouldIntercept) {
return { // server info from the original request
const originServer = {
host, 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 { } else {
return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port })); 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 { class RequestHandler {
/** /**
@ -621,6 +820,7 @@ class RequestHandler {
* @param {object} config * @param {object} config
* @param {boolean} config.forceProxyHttps proxy all https requests * @param {boolean} config.forceProxyHttps proxy all https requests
* @param {boolean} config.dangerouslyIgnoreUnauthorized * @param {boolean} config.dangerouslyIgnoreUnauthorized
@param {number} config.httpServerPort the http port AnyProxy do the proxy
* @param {object} rule * @param {object} rule
* @param {Recorder} recorder * @param {Recorder} recorder
* *
@ -628,22 +828,36 @@ class RequestHandler {
*/ */
constructor(config, rule, recorder) { constructor(config, rule, recorder) {
const reqHandlerCtx = this; const reqHandlerCtx = this;
this.forceProxyHttps = false;
this.dangerouslyIgnoreUnauthorized = false;
this.httpServerPort = '';
this.wsIntercept = false;
if (config.forceProxyHttps) { if (config.forceProxyHttps) {
this.forceProxyHttps = true; this.forceProxyHttps = true;
} }
if (config.dangerouslyIgnoreUnauthorized) { if (config.dangerouslyIgnoreUnauthorized) {
this.dangerouslyIgnoreUnauthorized = true; this.dangerouslyIgnoreUnauthorized = true;
} }
if (config.wsIntercept) {
this.wsIntercept = config.wsIntercept;
}
this.httpServerPort = config.httpServerPort;
const default_rule = util.freshRequire('./rule_default'); const default_rule = util.freshRequire('./rule_default');
const userRule = util.merge(default_rule, rule); const userRule = util.merge(default_rule, rule);
reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]); reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]);
reqHandlerCtx.wsHandler = getWsHandler.bind(this, userRule, recorder);
reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({ 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]);
} }
} }

View File

@ -1,12 +1,12 @@
'use strict'; 'use strict';
module.exports = { module.exports = {
summary: 'the default rule for AnyProxy', summary: 'the default rule for AnyProxy',
/** /**
* *
* *
* @param {object} requestDetail * @param {object} requestDetail
* @param {string} requestDetail.protocol * @param {string} requestDetail.protocol
* @param {object} requestDetail.requestOptions * @param {object} requestDetail.requestOptions
@ -23,8 +23,8 @@ module.exports = {
/** /**
* *
* *
* @param {object} requestDetail * @param {object} requestDetail
* @param {object} responseDetail * @param {object} responseDetail
*/ */
@ -34,21 +34,22 @@ module.exports = {
/** /**
* * default to return null
* * the user MUST return a boolean when they do implement the interface in rule
* @param {any} requestDetail *
* @returns * @param {any} requestDetail
* @returns
*/ */
*beforeDealHttpsRequest(requestDetail) { *beforeDealHttpsRequest(requestDetail) {
return false; return null;
}, },
/** /**
* *
* *
* @param {any} requestDetail * @param {any} requestDetail
* @param {any} error * @param {any} error
* @returns * @returns
*/ */
*onError(requestDetail, error) { *onError(requestDetail, error) {
return null; return null;
@ -56,11 +57,11 @@ module.exports = {
/** /**
* *
* *
* @param {any} requestDetail * @param {any} requestDetail
* @param {any} error * @param {any} error
* @returns * @returns
*/ */
*onConnectError(requestDetail, error) { *onConnectError(requestDetail, error) {
return null; return null;

View File

@ -4,8 +4,8 @@ const fs = require('fs'),
path = require('path'), path = require('path'),
mime = require('mime-types'), mime = require('mime-types'),
color = require('colorful'), color = require('colorful'),
crypto = require('crypto'),
Buffer = require('buffer').Buffer, Buffer = require('buffer').Buffer,
configUtil = require('./configUtil'),
logUtil = require('./log'); logUtil = require('./log');
const networkInterfaces = require('os').networkInterfaces(); const networkInterfaces = require('os').networkInterfaces();
@ -35,7 +35,7 @@ function getUserHome() {
module.exports.getUserHome = getUserHome; module.exports.getUserHome = getUserHome;
function getAnyProxyHome() { function getAnyProxyHome() {
const home = configUtil.getAnyProxyHome(); const home = path.join(getUserHome(), '/.anyproxy/');
if (!fs.existsSync(home)) { if (!fs.existsSync(home)) {
fs.mkdirSync(home); fs.mkdirSync(home);
} }
@ -306,3 +306,18 @@ module.exports.isIpDomain = function (domain) {
return ipReg.test(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');
}

View File

@ -128,6 +128,7 @@ class webInterface extends events.EventEmitter {
res.json({ res.json({
id: query.id, id: query.id,
type: result.type, type: result.type,
method: result.meethod,
fileName: result.fileName, fileName: result.fileName,
ref: `/downloadBody?id=${query.id}&download=${isDownload}&raw=${!isDownload}` ref: `/downloadBody?id=${query.id}&download=${isDownload}&raw=${!isDownload}`
}); });
@ -143,6 +144,7 @@ class webInterface extends events.EventEmitter {
res.json({ res.json({
id: query.id, id: query.id,
type: result.type, type: result.type,
method: result.method,
resBody: result.content 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) => { app.get('/fetchCrtFile', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
const _crtFilePath = certMgr.getRootCAFilePath(); const _crtFilePath = certMgr.getRootCAFilePath();

View File

@ -99,7 +99,11 @@ class wsServer {
wss.broadcast = function (data) { wss.broadcast = function (data) {
if (typeof data === 'object') { 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) { for (const client of wss.clients) {
try { 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; self.wss = wss;
}); });
} }

39
lib/wsServerMgr.js Normal file
View File

@ -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;

View File

@ -21,6 +21,7 @@
"compression": "^1.4.4", "compression": "^1.4.4",
"es6-promise": "^3.3.1", "es6-promise": "^3.3.1",
"express": "^4.8.5", "express": "^4.8.5",
"fast-json-stringify": "^0.17.0",
"iconv-lite": "^0.4.6", "iconv-lite": "^0.4.6",
"inquirer": "^3.0.1", "inquirer": "^3.0.1",
"ip": "^0.3.2", "ip": "^0.3.2",

View File

@ -11,6 +11,7 @@ const http = require('http'),
events = require('events'), events = require('events'),
co = require('co'), co = require('co'),
WebInterface = require('./lib/webInterface'), WebInterface = require('./lib/webInterface'),
wsServerMgr = require('./lib/wsServerMgr'),
ThrottleGroup = require('stream-throttle').ThrottleGroup; ThrottleGroup = require('stream-throttle').ThrottleGroup;
// const memwatch = require('memwatch-next'); // 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.silent=false] - if keep the console silent
* @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response * @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response
* @param {object} [config.recorder] - recorder to use * @param {object} [config.recorder] - recorder to use
* @param {boolean} [config.wsIntercept] - whether intercept websocket
* *
* @memberOf ProxyCore * @memberOf ProxyCore
*/ */
@ -114,6 +116,8 @@ class ProxyCore extends events.EventEmitter {
// init request handler // init request handler
const RequestHandler = util.freshRequire('./requestHandler'); const RequestHandler = util.freshRequire('./requestHandler');
this.requestHandler = new RequestHandler({ this.requestHandler = new RequestHandler({
wsIntercept: config.wsIntercept,
httpServerPort: config.port, // the http server port for http proxy
forceProxyHttps: !!config.forceProxyHttps, forceProxyHttps: !!config.forceProxyHttps,
dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized
}, this.proxyRule, this.recorder); }, this.proxyRule, this.recorder);
@ -185,6 +189,10 @@ class ProxyCore extends events.EventEmitter {
}, },
function (callback) { function (callback) {
wsServerMgr.getWsServer({
server: self.httpProxyServer,
connHandler: self.requestHandler.wsHandler
});
// remember all sockets, so we can destory them when call the method 'close'; // remember all sockets, so we can destory them when call the method 'close';
self.httpProxyServer.on('connection', (socket) => { self.httpProxyServer.on('connection', (socket) => {
self.handleExistConnections.call(self, socket); self.handleExistConnections.call(self, socket);
@ -324,17 +332,10 @@ class ProxyServer extends ProxyCore {
this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, this.recorder); this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, this.recorder);
} }
new Promise((resolve) => { // start web server
// start web server this.webServerInstance.start().then(() => {
if (this.webServerInstance) {
resolve(this.webServerInstance.start());
} else {
resolve(null);
}
})
.then(() => {
// start proxy core // start proxy core
super.start() super.start();
}) })
.catch((e) => { .catch((e) => {
this.emit('error', e); this.emit('error', e);

View File

@ -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); self.httpsServer.listen(HTTPS_PORT);

View File

@ -6,23 +6,32 @@
const ProxyServerUtil = require('../util/ProxyServerUtil.js'); const ProxyServerUtil = require('../util/ProxyServerUtil.js');
const { generateWsUrl, directWs, proxyWs } = require('../util/HttpUtil.js'); const { generateWsUrl, directWs, proxyWs } = require('../util/HttpUtil.js');
const Server = require('../server/server.js'); const Server = require('../server/server.js');
const { printLog } = require('../util/CommonUtil.js'); const { printLog, isArrayEqual } = require('../util/CommonUtil.js');
testWebsocket('ws'); testWebsocket('ws');
testWebsocket('wss'); testWebsocket('wss');
testWebsocket('ws', true);
testWebsocket('wss', true);
function testWebsocket(protocol) { function testWebsocket(protocol, masked = false) {
describe('Test WebSocket in protocol : ' + protocol, () => { describe('Test WebSocket in protocol : ' + protocol, () => {
const url = generateWsUrl(protocol, '/test/socket'); const url = generateWsUrl(protocol, '/test/socket');
let serverInstance; let serverInstance;
let proxyServer; 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) => { beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
printLog('Start server for no_rule_websocket_spec'); printLog('Start server for no_rule_websocket_spec');
serverInstance = new Server(); serverInstance = new Server();
proxyServer = ProxyServerUtil.proxyServerWithoutHttpsIntercept(); proxyServer = ProxyServerUtil.defaultProxyServer();
setTimeout(() => { setTimeout(() => {
done(); done();
@ -36,32 +45,57 @@ function testWebsocket(protocol) {
}); });
it('Default websocket option', done => { it('Default websocket option', done => {
const sendMessage = 'Send the message with default option'; const directMessages = []; // set the flag for direct message, compare when both direct and proxy got message
let directMessage; // set the flag for direct message, compare when both direct and proxy got message const proxyMessages = [];
let proxyMessage; let directHeaders;
let proxyHeaders;
const ws = directWs(url); const ws = directWs(url);
const porxyWsRef = proxyWs(url); const proxyWsRef = proxyWs(url);
ws.on('open', () => { 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', () => { proxyWsRef.on('open', () => {
porxyWsRef.send(sendMessage); 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) => { ws.on('message', (data, flag) => {
const message = JSON.parse(data); const message = JSON.parse(data);
if (message.type === 'onMessage') { if (message.type === 'onMessage') {
directMessage = message.content; directMessages.push(message.content);
compareMessageIfReady(); compareMessageIfReady();
} }
}); });
porxyWsRef.on('message', (data, flag) => { proxyWsRef.on('message', (data, flag) => {
const message = JSON.parse(data); const message = JSON.parse(data);
if (message.type === 'onMessage') { if (message.type === 'onMessage') {
proxyMessage = message.content; proxyMessages.push(message.content);
compareMessageIfReady(); compareMessageIfReady();
} }
}); });
@ -71,70 +105,24 @@ function testWebsocket(protocol) {
done.fail('Error happened in direct websocket'); done.fail('Error happened in direct websocket');
}); });
porxyWsRef.on('error', error => { proxyWsRef.on('error', error => {
console.error('error happened in proxy websocket:', error); console.error('error happened in proxy websocket:', error);
done.fail('Error happened in proxy websocket'); done.fail('Error happened in proxy websocket');
}); });
function compareMessageIfReady() { function compareMessageIfReady() {
if (directMessage && proxyMessage) { const targetLen = testMessageArray.length;
expect(directMessage).toEqual(proxyMessage); if (directMessages.length === targetLen
expect(directMessage).toEqual(sendMessage); && proxyMessages.length === targetLen
done(); && directHeaders && proxyHeaders
} ) {
} expect(isArrayEqual(directMessages, testMessageArray)).toBe(true);
}); expect(isArrayEqual(directMessages, proxyMessages)).toBe(true);
expect(directHeaders['x-anyproxy-websocket']).toBeUndefined();
it('masked:true', done => { expect(proxyHeaders['x-anyproxy-websocket']).toBe('true');
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);
done(); done();
} }
} }
}); });
}); });
} }

View File

@ -264,5 +264,6 @@ module.exports = {
printHilite, printHilite,
isCommonReqEqual, isCommonReqEqual,
parseUrlQuery, parseUrlQuery,
stringSimilarity stringSimilarity,
isArrayEqual: _isDeepEqual
}; };

View File

@ -13,6 +13,7 @@ const DEFAULT_OPTIONS = {
webPort: 8002, // optional, port for web interface webPort: 8002, // optional, port for web interface
wsPort: 8003, // optional, internal port for web socket wsPort: 8003, // optional, internal port for web socket
}, },
wsIntercept: true,
throttle: 10000, // optional, speed limit in kb/s throttle: 10000, // optional, speed limit in kb/s
forceProxyHttps: true, // intercept https as well forceProxyHttps: true, // intercept https as well
dangerouslyIgnoreUnauthorized: true, dangerouslyIgnoreUnauthorized: true,

View File

@ -4,12 +4,21 @@
*/ */
import { message } from 'antd'; 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){ if(!WebSocket){
throw (new Error('WebSocket is not supportted on this browser')); 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) => { wsClient.onerror = (error) => {
console.error(error); console.error(error);
@ -30,4 +39,3 @@ export function initWs(wsPort = 8003, key = '') {
export default { export default {
initWs: initWs initWs: initWs
}; };

View File

@ -11,10 +11,11 @@ export function formatDate(date, formatter) {
if (typeof date !== 'object') { if (typeof date !== 'object') {
date = new Date(date); date = new Date(date);
} }
const transform = function(value) { const transform = function(value) {
return value < 10 ? '0' + value : 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) { switch (match) {
case 'YYYY': case 'YYYY':
return transform(date.getFullYear()); return transform(date.getFullYear());
@ -28,6 +29,8 @@ export function formatDate(date, formatter) {
return transform(date.getHours()); return transform(date.getHours());
case 'ss': case 'ss':
return transform(date.getSeconds()); return transform(date.getSeconds());
case 'ms':
return transform(date.getMilliseconds());
} }
}); });
} }

View File

@ -5,15 +5,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ClassBind from 'classnames/bind'; import ClassBind from 'classnames/bind';
import { Menu, Table, notification, Spin } from 'antd'; import { Menu, Spin } from 'antd';
import clipboard from 'clipboard-js'
import JsonViewer from 'component/json-viewer';
import ModalPanel from 'component/modal-panel'; import ModalPanel from 'component/modal-panel';
import RecordRequestDetail from 'component/record-request-detail'; import RecordRequestDetail from 'component/record-request-detail';
import RecordResponseDetail from 'component/record-response-detail'; import RecordResponseDetail from 'component/record-response-detail';
import RecordWsMessageDetail from 'component/record-ws-message-detail';
import { hideRecordDetail } from 'action/recordAction'; import { hideRecordDetail } from 'action/recordAction';
import { selectText } from 'common/CommonUtil';
import { curlify } from 'common/curlUtil';
import Style from './record-detail.less'; import Style from './record-detail.less';
import CommonStyle from '../style/common.less'; import CommonStyle from '../style/common.less';
@ -21,7 +18,8 @@ import CommonStyle from '../style/common.less';
const StyleBind = ClassBind.bind(Style); const StyleBind = ClassBind.bind(Style);
const PageIndexMap = { const PageIndexMap = {
REQUEST_INDEX: 'REQUEST_INDEX', 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 // 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) { getRequestDiv(recordDetail) {
return <RecordRequestDetail recordDetail={recordDetail} />; return <RecordRequestDetail recordDetail={recordDetail} />;
} }
@ -62,18 +64,45 @@ class RecordDetail extends React.Component {
return <RecordResponseDetail recordDetail={recordDetail} />; return <RecordResponseDetail recordDetail={recordDetail} />;
} }
getRecordContentDiv(recordDetail, fetchingRecord) { getWsMessageDiv(recordDetail) {
const { globalStatus } = this.props;
return <RecordWsMessageDetail recordDetail={recordDetail} wsPort={globalStatus.wsPort} />;
}
getRecordContentDiv(recordDetail = {}, fetchingRecord) {
const getMenuBody = () => { const getMenuBody = () => {
const menuBody = this.state.pageIndex === PageIndexMap.REQUEST_INDEX ? let menuBody = null;
this.getRequestDiv(recordDetail) : this.getResponseDiv(recordDetail); switch (this.state.pageIndex) {
case PageIndexMap.REQUEST_INDEX: {
menuBody = this.getRequestDiv(recordDetail);
break;
}
case PageIndexMap.RESPONSE_INDEX: {
menuBody = this.getResponseDiv(recordDetail);
break;
}
case PageIndexMap.WEBSOCKET_INDEX: {
menuBody = this.getWsMessageDiv(recordDetail);
break;
}
default: {
menuBody = this.getRequestDiv(recordDetail);
break;
}
}
return menuBody; return menuBody;
} }
const websocketMenu = (
<Menu.Item key={PageIndexMap.WEBSOCKET_INDEX}>WebSocket</Menu.Item>
);
return ( return (
<div className={Style.wrapper} > <div className={Style.wrapper} >
<Menu onClick={this.onMenuChange} mode="horizontal" selectedKeys={[this.state.pageIndex]} > <Menu onClick={this.onMenuChange} mode="horizontal" selectedKeys={[this.state.pageIndex]} >
<Menu.Item key={PageIndexMap.REQUEST_INDEX}>Request</Menu.Item> <Menu.Item key={PageIndexMap.REQUEST_INDEX}>Request</Menu.Item>
<Menu.Item key={PageIndexMap.RESPONSE_INDEX}>Response</Menu.Item> <Menu.Item key={PageIndexMap.RESPONSE_INDEX}>Response</Menu.Item>
{this.hasWebSocket(recordDetail) ? websocketMenu : null}
</Menu> </Menu>
<div className={Style.detailWrapper} > <div className={Style.detailWrapper} >
{fetchingRecord ? this.getLoaingDiv() : getMenuBody()} {fetchingRecord ? this.getLoaingDiv() : getMenuBody()}
@ -92,8 +121,9 @@ class RecordDetail extends React.Component {
} }
getRecordDetailDiv() { getRecordDetailDiv() {
const recordDetail = this.props.requestRecord.recordDetail; const { requestRecord, globalStatus } = this.props;
const fetchingRecord = this.props.globalStatus.fetchingRecord; const recordDetail = requestRecord.recordDetail;
const fetchingRecord = globalStatus.fetchingRecord;
if (!recordDetail && !fetchingRecord) { if (!recordDetail && !fetchingRecord) {
return null; return null;
@ -101,6 +131,17 @@ class RecordDetail extends React.Component {
return this.getRecordContentDiv(recordDetail, fetchingRecord); return this.getRecordContentDiv(recordDetail, fetchingRecord);
} }
componentWillReceiveProps(nextProps) {
const { requestRecord } = nextProps;
const { pageIndex } = this.state;
// if this is not websocket, reset the index to RESPONSE_INDEX
if (!this.hasWebSocket(requestRecord.recordDetail) && pageIndex === PageIndexMap.WEBSOCKET_INDEX) {
this.setState({
pageIndex: PageIndexMap.RESPONSE_INDEX
});
}
}
render() { render() {
return ( return (
<ModalPanel <ModalPanel

View File

@ -2,6 +2,7 @@
.wrapper { .wrapper {
padding: 5px 15px; padding: 5px 15px;
height: 100%;
word-wrap: break-word; word-wrap: break-word;
} }
@ -16,6 +17,8 @@
} }
.detailWrapper { .detailWrapper {
position: relative;
min-height: 100%;
padding: 5px; padding: 5px;
} }

View File

@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
} }
static propTypes = { static propTypes = {
requestRecord: PropTypes.object recordDetail: PropTypes.object
} }
onSelectText(e) { onSelectText(e) {

View File

@ -0,0 +1,147 @@
/**
* The panel to display the detial of the record
*
*/
import React, { PropTypes } from 'react';
import { message, Button, Icon } from 'antd';
import { formatDate } from 'common/CommonUtil';
import { initWs } from 'common/WsUtil';
import ClassBind from 'classnames/bind';
import Style from './record-ws-message-detail.less';
import CommonStyle from '../style/common.less';
const ToMessage = (props) => {
const { message: wsMessage } = props;
return (
<div className={Style.toMessage}>
<div className={`${Style.time} ${CommonStyle.right}`}>{formatDate(wsMessage.time, 'hh:mm:ss:ms')}</div>
<div className={Style.content}>{wsMessage.message}</div>
</div>
);
}
const FromMessage = (props) => {
const { message: wsMessage } = props;
return (
<div className={Style.fromMessage}>
<div className={Style.time}>{formatDate(wsMessage.time, 'hh:mm:ss:ms')}</div>
<div className={Style.content}>{wsMessage.message}</div>
</div>
);
}
class RecordWsMessageDetail extends React.Component {
constructor() {
super();
this.state = {
stateCheck: false, // a prop only to trigger state check
autoRefresh: true,
socketMessages: [] // the messages from websocket listening
};
this.updateStateRef = null; // a timeout ref to reduce the calling of update state
this.wsClient = null; // ref to the ws client
this.onMessageHandler = this.onMessageHandler.bind(this);
this.receiveNewMessage = this.receiveNewMessage.bind(this);
this.toggleRefresh = this.toggleRefresh.bind(this);
}
static propTypes = {
recordDetail: PropTypes.object,
wsPort: PropTypes.number
}
toggleRefresh () {
const { autoRefresh } = this.state;
this.state.autoRefresh = !autoRefresh;
this.setState({
stateCheck: true
});
}
receiveNewMessage (message) {
this.state.socketMessages.push(message);
this.updateStateRef && clearTimeout(this.updateStateRef);
this.updateStateRef = setTimeout(() => {
this.setState({
stateCheck: true
});
}, 100);
}
getMessageList () {
const { recordDetail } = this.props;
const { socketMessages } = this.state;
const { wsMessages = [] } = recordDetail;
const targetMessage = wsMessages.concat(socketMessages);
return targetMessage.map((messageItem, index) => {
return messageItem.isToServer ?
<ToMessage key={index} message={messageItem} /> : <FromMessage key={index} message={messageItem} />;
});
}
refreshPage () {
const { autoRefresh } = this.state;
if (autoRefresh && this.messageRef && this.messageContentRef) {
this.messageRef.scrollTop = this.messageContentRef.scrollHeight;
}
}
onMessageHandler (event) {
const { recordDetail } = this.props;
const data = JSON.parse(event.data);
const content = data.content;
if (data.type === 'updateLatestWsMsg' ) {
if (recordDetail.id === content.id) {
this.receiveNewMessage(content.message);
}
}
}
componentDidUpdate () {
this.refreshPage();
}
componentWillUnmount () {
this.wsClient && this.wsClient.removeEventListener('message', this.onMessageHandler);
}
componentDidMount () {
const { wsPort, recordDetail } = this.props;
if (!wsPort) {
return;
}
this.refreshPage();
this.wsClient = initWs(wsPort);
this.wsClient.addEventListener('message', this.onMessageHandler);
}
render() {
const { recordDetail } = this.props;
const { autoRefresh } = this.state;
if (!recordDetail) {
return null;
}
const playIcon = <Icon type="play-circle" />;
const pauseIcon = <Icon type="pause-circle" />;
return (
<div className={Style.wrapper} ref={(_ref) => this.messageRef = _ref}>
<div className={Style.contentWrapper} ref={(_ref) => this.messageContentRef = _ref}>
{this.getMessageList()}
</div>
<div className={Style.refreshBtn} onClick={this.toggleRefresh} >
{autoRefresh ? pauseIcon : playIcon}
</div>
</div>
);
}
}
export default RecordWsMessageDetail;

View File

@ -0,0 +1,56 @@
@import '../style/constant.less';
.wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
}
.contentWrapper {
overflow: hidden;
}
.toMessage {
float: right;
clear: both;
margin: 5px auto;
.content {
background-color: @primary-color;
}
}
.fromMessage {
float: left;
clear: both;
max-width: 40%;
margin: 5px auto;
.content {
background: @success-color;
}
}
.time {
font-size: @font-size-xs;
color: @tip-color;
}
.content {
clear: both;
border-radius: @border-radius-base;
color: #fff;
padding: 7px 8px;
font-size: @font-size-sm;
word-wrap: break-word;
word-break: break-all;
}
.refreshBtn {
position: fixed;
right: 20px;
bottom: 5px;
opacity: 0.53;
font-size: @font-size-large;
cursor: pointer;
}

View File

@ -50,6 +50,9 @@ function* doFetchRecordBody(recordId) {
// const recordBody = { id: recordId }; // const recordBody = { id: recordId };
yield put(updateFechingRecordStatus(true)); yield put(updateFechingRecordStatus(true));
const recordBody = yield call(getJSON, '/fetchBody', { id: recordId }); const recordBody = yield call(getJSON, '/fetchBody', { id: recordId });
if (recordBody.method && recordBody.method.toLowerCase() === 'websocket') {
recordBody.wsMessages = yield call(getJSON, '/fetchWsMessages', { id: recordId});
}
recordBody.id = parseInt(recordBody.id, 10); recordBody.id = parseInt(recordBody.id, 10);
yield put(updateFechingRecordStatus(false)); yield put(updateFechingRecordStatus(false));

View File

@ -54,13 +54,13 @@ body {
} }
:global { :global {
.ant-btn { // .ant-btn {
min-width: 100px; // min-width: 100px;
} // }
} }
.relativeWrapper { .relativeWrapper {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }