mirror of
https://github.com/alibaba/anyproxy.git
synced 2025-04-23 15:51:25 +00:00
Merge pull request #324 from alibaba/ws-proxy
add ws and wss support to AnyProxy
This commit is contained in:
commit
b2fc71c3b7
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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`
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
23
lib/log.js
23
lib/log.js
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
19
lib/util.js
19
lib/util.js
@ -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');
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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
39
lib/wsServerMgr.js
Normal 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;
|
@ -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",
|
||||||
|
21
proxy.js
21
proxy.js
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,5 +264,6 @@ module.exports = {
|
|||||||
printHilite,
|
printHilite,
|
||||||
isCommonReqEqual,
|
isCommonReqEqual,
|
||||||
parseUrlQuery,
|
parseUrlQuery,
|
||||||
stringSimilarity
|
stringSimilarity,
|
||||||
|
isArrayEqual: _isDeepEqual
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
requestRecord: PropTypes.object
|
recordDetail: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectText(e) {
|
onSelectText(e) {
|
||||||
|
147
web/src/component/record-ws-message-detail.jsx
Normal file
147
web/src/component/record-ws-message-detail.jsx
Normal 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;
|
56
web/src/component/record-ws-message-detail.less
Normal file
56
web/src/component/record-ws-message-detail.less
Normal 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;
|
||||||
|
}
|
@ -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));
|
||||||
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user