whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
434 lines (415 loc) • 12.3 kB
JavaScript
var EventEmitter = require('events').EventEmitter;
var zlib = require('../util/zlib');
var util = require('../util');
var socketMgr = require('../socket-mgr');
var config = require('../config');
var MAX_BODY_SIZE = 360 * 1024;
var MAX_SIZE = (config.strict ? 256 : 1024) * 1024;
var MAX_REQ_BODY_SIZE = (config.strict ? 256 : 2048) * 1024;
var MAX_RES_BODY_SIZE = (config.strict ? 256 : 2048) * 1024;
var BIG_DATA_SIZE = 1024 * 1024 * 16;
var LOCALHOST = '127.0.0.1';
var SSE_RE = /^\s*text\/event-stream/i;
function getZipType(options) {
return options.headers && options.headers['content-encoding'];
}
function unzipBody(options, body, callback) {
return zlib.unzip(getZipType(options), body, callback);
}
function checkType(res) {
if (!config.strict) {
return true;
}
var type = res.headers['content-type'];
if (!type) {
return true;
}
type = util.getContentType(type);
return type && type !== 'CSS' && type !== 'IMG';
}
function checkBodySize(data, useBigData) {
if (
config.strict &&
data.body &&
data.body.length > (useBigData ? BIG_DATA_SIZE : MAX_BODY_SIZE)
) {
data.body = '';
}
}
function getEventName(proxy) {
if (util.listenerCount(proxy, '_request')) {
return '_request';
} else if (util.listenerCount(proxy, 'httpRequest')) {
return 'httpRequest';
}
}
function emitDataEvents(req, res, proxy) {
var now = Date.now();
var eventName = getEventName(proxy);
eventName && proxy.emit(eventName, req.fullUrl);
if (!util.showPluginReq(req) || util.isHide(req)) {
return;
}
var getTTFB = function () {
if (req._ttfb >= now) {
return req._ttfb - now;
}
};
var _res = {};
var reqEmitter = new EventEmitter();
var reqData = {
method: util.toUpperCase(req.method) || 'GET',
httpVersion: req.httpVersion || '1.1',
ip: req.clientIp,
port: req.clientPort,
isWhistleHttps: req.isWhistleHttps,
rawHeaderNames: req.rawHeaderNames,
headers: req.headers
};
var resData = {};
var reqBody = false;
var resBody = false;
var cleared;
var data = {
useH2: req.isH2,
h2Id: req._alpn,
fc: req.fromComposer ? 1 : undefined,
isPR: req.isPluginReq ? 1 : undefined,
_clientId: req._clientId,
startTime: now,
ttfb: getTTFB(),
id: req.reqId,
sniPlugin: req.sniPlugin,
url: req.fullUrl,
req: reqData,
res: resData,
rules: req.rules,
fwdHost: req._fwdHost,
pipe: req._pipeRule,
rulesHeaders: req.rulesHeaders,
abort: function (clear) {
if (clear === true) {
data = reqData = resData = reqBody = resBody = false;
cleared = true;
} else {
var err = new Error('Aborted');
err.code = 'ERR_WHISTLE_ABORTED';
req.emit('error', err);
}
}
};
proxy.emit('request', reqEmitter, data);
var requestTime;
var endTime;
var updateEvent;
var useFrames;
var enable = req.enable;
var disable = req.disable;
var useBigData =
(enable.bigData || enable.largeData) &&
!disable.bigData &&
!disable.largeData;
var setRequestTime = function (time) {
data.requestTime = time;
data.connectTime = req._connectTime;
};
var setEndTime = function () {
if (data.requestTime || !requestTime) {
data.endTime = endTime || Date.now();
setRequestTime(data.requestTime || data.endTime);
} else if (endTime == null) {
endTime = endTime || Date.now();
}
};
var updateVersion = function () {
if (req.useH2 != null) {
data.useH2 = req.useH2;
}
data.httpsTime = req.httpsTime;
data.useHttp = req.useHttp;
if (data.useH2 && !req.useHttp) {
reqData.httpVersion = '2.0';
} else {
reqData.httpVersion = '1.1';
}
};
var setUnzipSize = function (body, obj) {
var len = body ? body.length : -1;
if (len >= 0 && len !== obj.size) {
obj.unzipSize = len;
}
};
req.setServerPort = function (serverPort) {
req.serverPort = serverPort;
setReqStatus(LOCALHOST);
resData.port = serverPort;
};
req.setClientId = function (clientId) {
data.clientId = clientId;
};
req.setTTFB = function() {
data.ttfb = getTTFB();
};
res.setCurTrailers = function (trailers, rawTrailerNames) {
resData.trailers = trailers;
resData.rawTrailerNames = rawTrailerNames;
};
var reqDone;
var resDone;
var captureReqStream;
var maxReqSize;
var reqInfo;
var updateReqInfo = function() {
if (reqInfo && !captureReqStream) {
captureReqStream = !disable.captureStream && (resDone || enable.captureStream) && !getZipType(reqInfo);
maxReqSize = useBigData ? BIG_DATA_SIZE : (captureReqStream ? MAX_REQ_BODY_SIZE : MAX_SIZE);
if (captureReqStream && reqBody && !reqData.body) {
reqData.body = reqBody;
}
}
};
var handleReqBody = function (stream, info) {
if (reqDone) {
return;
}
reqDone = true;
reqInfo = info || (req._needGunzip ? { method: req.method, headers: '' } : req);
if (!cleared && util.hasRequestBody(reqInfo)) {
reqBody = null;
}
reqData.size = 0;
updateReqInfo();
var write = stream.write;
var end = stream.end;
stream.write = function (chunk) {
if (chunk) {
if (reqBody || reqBody === null) {
if (useFrames) {
reqBody = '';
} else {
reqBody = reqBody ? Buffer.concat([reqBody, chunk]) : chunk;
if (captureReqStream) {
reqData.body = reqBody;
}
if (reqBody.length > maxReqSize) {
reqBody = false;
}
}
}
reqData.size += chunk.length;
}
updateEvent && proxy.emit(updateEvent, req.fullUrl, false);
return write.apply(this, arguments);
};
stream.end = function (chunk) {
if (chunk) {
reqData.size += chunk.length;
reqBody = reqBody ? Buffer.concat([reqBody, chunk]) : chunk;
}
requestTime = Date.now();
if (useFrames) {
reqBody = '';
}
unzipBody(reqInfo, reqBody, function (err, body) {
setRequestTime(requestTime);
if (endTime) {
data.endTime = endTime;
}
reqBody = err ? util.getErrorStack(err) : body;
reqData.body = reqBody;
setUnzipSize(body, reqData);
checkBodySize(reqData, useBigData);
});
updateEvent && proxy.emit(updateEvent, req.fullUrl, false, true);
return end.apply(this, arguments);
};
};
var handleResBody = function (stream, info) {
if (resDone) {
return;
}
resDone = true;
info = info || (res._needGunzip ? { statusCode: res.statusCode, headers: '' } : res);
var captureStream = !disable.captureStream && (requestTime == null || enable.captureStream ||
(info.headers && SSE_RE.test(info.headers['content-type']))) && !getZipType(info);
var maxResSize = useBigData ? BIG_DATA_SIZE : (captureStream ? MAX_RES_BODY_SIZE : MAX_SIZE);
captureStream && updateReqInfo();
if (!cleared && util.hasBody(info, req) && checkType(info)) {
if (info.headers['content-length'] > maxResSize && !captureStream) {
resBody = false;
} else {
resBody = null;
}
}
resData.size = 0;
var write = stream.write;
var end = stream.end;
stream.write = function (chunk) {
if (chunk) {
if (resBody || resBody === null) {
if (useFrames) {
resBody = '';
} else {
resBody = resBody ? Buffer.concat([resBody, chunk]) : chunk;
if (captureStream) {
resData.body = resBody;
}
if (resBody.length > maxResSize) {
resBody = false;
}
}
}
resData.size += chunk.length;
}
updateEvent && proxy.emit(updateEvent, req.fullUrl, true);
return write.apply(this, arguments);
};
stream.end = function (chunk) {
if (chunk) {
resData.size += chunk.length;
resBody = resBody ? Buffer.concat([resBody, chunk]) : chunk;
}
endTime = Date.now();
delete data.abort;
if (useFrames) {
resBody = '';
}
resData.hasGzipError = unzipBody(info, resBody, function (err, body) {
if (!data.resError) {
resBody = err ? util.getErrorStack(err) : body;
resData.body = resBody;
}
setUnzipSize(body, resData);
checkBodySize(resData, useBigData);
setEndTime();
reqEmitter.emit('end', data);
});
updateEvent && proxy.emit(updateEvent, req.fullUrl, true, true);
return end.apply(this, arguments);
};
};
var hasReqPipe = req._pipePluginPorts.reqWritePort;
var hasResPipe = req._pipePluginPorts.resWritePort;
if (hasReqPipe) {
req.on('bodyStreamReady', handleReqBody);
}
if (hasResPipe) {
res.on('bodyStreamReady', handleResBody);
}
req.once('dest', function (_req) {
_req.once('finish', function () {
if (!requestTime) {
setRequestTime(requestTime);
}
});
setReqStatus();
reqEmitter.emit('send', data);
!hasReqPipe && handleReqBody(_req, req);
});
res.once('src', function (r) {
_res = r;
data.pipe = req._pipeRule;
resData.rawHeaderNames = res.rawHeaderNames;
if (!data.endTime) {
setResStatus();
reqEmitter.emit('response', data);
}
!hasResPipe && handleResBody(res, _res);
});
req.once('error', handleError);
res.once('error', handleError);
res.once('close', handleError);
res.once('finish', setEndTime);
function handleError(err) {
req._hasError = true;
if (!data || endTime || data.endTime || (data.responseTime && !err)) {
return;
}
!endTime && setEndTime();
delete data.abort;
if (err && err.message !== 'Aborted') {
data.resError = true;
if (resData.body) {
data.errMsg = util.getErrorStack(err);
} else {
resData.body = util.getErrorStack(err);
}
util.emitError(reqEmitter, data);
setResStatus(502);
} else {
data.reqError = true;
if (!reqData.body) {
reqData.body = 'aborted';
} else if (!resData.body) {
resData.body = 'aborted';
} else {
data.errMsg = 'aborted';
}
if (req.__resHeaders) {
_res.headers = req.__resHeaders;
}
reqEmitter.emit('abort', data);
setResStatus('aborted' + (req.__statusCode ? ' (' + req.__statusCode + ')' : ''));
}
}
function setReqStatus(defaultHost) {
data.dnsTime = (req.dnsTime || 0) + now;
data.realUrl = data.url === req.realUrl ? undefined : req.realUrl;
updateVersion();
resData.ip = req.hostIp || defaultHost;
resData.phost = req._phost && req._phost.host;
}
req.initCustomParser = function () {
if (req.customParser) {
data.useFrames = false;
socketMgr.setPending(req);
req.disableCustomParser = function () {
data.useFrames = null;
req.customParser = null;
req.enableCustomParser = null;
req.disableCustomParser = null;
socketMgr.removePending(req);
delete req.headers['x-whistle-frame-parser'];
};
req.enableCustomParser = function (svrRes) {
useFrames = true;
reqData.body = '';
resData.body = '';
reqData.base64 = '';
resData.base64 = '';
data.useFrames = true;
socketMgr.setContext(
req,
svrRes,
eventName,
{ data: [] },
{ data: [] }
);
socketMgr.removePending(req);
delete req.headers['x-whistle-frame-parser'];
};
}
};
function setResStatus(defaultCode) {
if (data.responseTime) {
return;
}
setReqStatus(LOCALHOST);
resData.statusCode = _res.statusCode || defaultCode || 502;
resData.statusMessage = _res.statusMessage;
data.responseTime = Date.now();
if (!requestTime && !data.requestTime) {
updateEvent = eventName;
}
resData.headers = _res.headers;
}
}
module.exports = function (req, res, next) {
emitDataEvents(req, res, this);
util.delay(util.getMatcherValue(req.rules.reqDelay), function () {
if (util.needAbortReq(req)) {
return res.destroy();
}
next();
});
};