mirror of
https://github.com/avwo/whistle.git
synced 2026-10-20 07:58:55 +08:00
420 lines
11 KiB
JavaScript
420 lines
11 KiB
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 : 768) * 1024;
|
|
var MAX_REQ_BODY_SIZE = (config.strict ? 256 : 1536) * 1024;
|
|
var MAX_RES_BODY_SIZE = (config.strict ? 256 : 1536) * 1024;
|
|
var BIG_DATA_SIZE = 1024 * 1024 * 10;
|
|
var LOCALHOST = '127.0.0.1';
|
|
|
|
function getZipType(options) {
|
|
return options.headers && options.headers['content-encoding'];
|
|
}
|
|
|
|
function unzipBody(options, body, callback) {
|
|
return zlib.unzip(getZipType(options), body, callback);
|
|
}
|
|
|
|
function getChunkLen(chunk) {
|
|
return (chunk && chunk.length) || 0;
|
|
}
|
|
|
|
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 isUnzipJs(r) {
|
|
r = r.headers;
|
|
return !r['content-encoding'] && util.getContentType(r) === 'JS';
|
|
}
|
|
|
|
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) ||
|
|
!config.captureData ||
|
|
(req._filters.hide && !req.disable.hide)
|
|
) {
|
|
return;
|
|
}
|
|
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,
|
|
isPR: req.isPluginReq ? 1 : undefined,
|
|
_clientId: req._clientId,
|
|
startTime: now,
|
|
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 isStream;
|
|
var updateEvent;
|
|
var useReqStream;
|
|
var useFrames;
|
|
var enable = req.enable;
|
|
var disable = req.disable;
|
|
var useBigData =
|
|
(enable.bigData || enable.largeData) &&
|
|
!disable.bigData &&
|
|
!disable.largeData;
|
|
var setEndTime = function () {
|
|
if (data.requestTime || !requestTime) {
|
|
data.endTime = endTime || Date.now();
|
|
data.requestTime = 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;
|
|
};
|
|
res.setCurTrailers = function (trailers, rawTrailerNames) {
|
|
resData.trailers = trailers;
|
|
resData.rawTrailerNames = rawTrailerNames;
|
|
};
|
|
var reqDone, resDone;
|
|
|
|
var handleReqBody = function (stream, info) {
|
|
if (reqDone) {
|
|
return;
|
|
}
|
|
reqDone = true;
|
|
info =
|
|
info || (req._needGunzip ? { method: req.method, headers: '' } : req);
|
|
if (!cleared && util.hasRequestBody(info)) {
|
|
reqBody = null;
|
|
}
|
|
reqData.size = 0;
|
|
var write = stream.write;
|
|
var end = stream.end;
|
|
var MAX_REQ_BODY = useBigData
|
|
? BIG_DATA_SIZE
|
|
: info.headers && info.headers['content-encoding']
|
|
? MAX_SIZE
|
|
: MAX_REQ_BODY_SIZE;
|
|
stream.write = function (chunk) {
|
|
if (chunk) {
|
|
if (reqBody || reqBody === null) {
|
|
if (useFrames) {
|
|
reqBody = '';
|
|
} else {
|
|
reqBody = reqBody ? Buffer.concat([reqBody, chunk]) : chunk;
|
|
if (useReqStream) {
|
|
reqData.body = reqBody;
|
|
}
|
|
if (reqBody.length > MAX_REQ_BODY) {
|
|
reqBody = false;
|
|
}
|
|
}
|
|
}
|
|
reqData.size += chunk.length;
|
|
}
|
|
updateEvent && proxy.emit(updateEvent, req.fullUrl, false);
|
|
return write.apply(this, arguments);
|
|
};
|
|
stream.end = function (chunk) {
|
|
reqData.size += getChunkLen(chunk);
|
|
requestTime = Date.now();
|
|
if (useFrames) {
|
|
reqBody = '';
|
|
}
|
|
unzipBody(info, reqBody, function (err, body) {
|
|
data.requestTime = 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 useStream = isStream && !getZipType(info);
|
|
var MAX_RES_BODY = useBigData
|
|
? BIG_DATA_SIZE
|
|
: isUnzipJs(info)
|
|
? MAX_RES_BODY_SIZE
|
|
: MAX_SIZE;
|
|
if (!cleared && util.hasBody(info, req) && checkType(info)) {
|
|
if (info.headers['content-length'] > MAX_RES_BODY) {
|
|
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 (useStream) {
|
|
resData.body = resBody;
|
|
}
|
|
if (resBody.length > MAX_RES_BODY) {
|
|
resBody = false;
|
|
}
|
|
}
|
|
}
|
|
resData.size += chunk.length;
|
|
}
|
|
updateEvent && proxy.emit(updateEvent, req.fullUrl, true);
|
|
return write.apply(this, arguments);
|
|
};
|
|
stream.end = function (chunk) {
|
|
resData.size += getChunkLen(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) {
|
|
data.requestTime = Date.now();
|
|
}
|
|
});
|
|
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 (endTime || data.endTime || (data.responseTime && !err)) {
|
|
return;
|
|
}
|
|
!endTime && setEndTime();
|
|
delete data.abort;
|
|
if (err && err.message !== 'Aborted') {
|
|
data.resError = true;
|
|
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';
|
|
}
|
|
reqEmitter.emit('abort', data);
|
|
setResStatus('aborted');
|
|
}
|
|
}
|
|
|
|
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) {
|
|
isStream = true;
|
|
data.isStream = true;
|
|
updateEvent = eventName;
|
|
useReqStream = !getZipType(req);
|
|
}
|
|
if (useReqStream && reqBody) {
|
|
reqData.body = reqBody;
|
|
}
|
|
resData.headers = _res.headers;
|
|
}
|
|
}
|
|
|
|
module.exports = function (req, res, next) {
|
|
emitDataEvents(req, res, this);
|
|
util.delay(util.getMatcherValue(req.rules.reqDelay), function () {
|
|
if (!req.disable.abort && (req._filters.abort || req.enable.abort)) {
|
|
return res.destroy();
|
|
}
|
|
next();
|
|
});
|
|
};
|