whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
1,789 lines (1,724 loc) • 79.8 kB
JavaScript
var listenerCount = require('../util/patch').listenerCount;
var path = require('path');
var fs = require('fs');
var net = require('net');
var qs = require('querystring');
var express = require('express');
var os = require('os');
var format = require('util').format;
var LRU = require('lru-cache');
var wsParser = require('ws-parser');
var http = require('http');
var https = require('https');
var extend = require('extend');
var gzip = require('zlib').gzip;
var request = require('../util/http-mgr').request;
var Storage = require('../rules/storage');
var getServer = require('hagent').create(null, 40500);
var parseUrl = require('../util/parse-url');
var hparser = require('hparser');
var transproto = require('../util/transproto');
var common = require('../util/common');
var getProxy = require('./proxy');
var rootRequire = require('../../require');
var extractSaz = require('../service/extract-saz');
var generateSaz = require('../service/generate-saz');
var getSharedStorage = require('./shared-storage');
var compat = require('./compat');
var getEncodeTransform = transproto.getEncodeTransform;
var getDecodeTransform = transproto.getDecodeTransform;
var setInternalOptions = common.setInternalOptions;
var toBuffer = common.toBuffer;
var readStream = common.readStream;
var LEVELS = ['log', 'error', 'warn', 'info', 'debug', 'trace'];
var HTTPS_RE = /^(?:https|wss):\/\//;
var MAX_BODY_SIZE = 1024 * 256;
var PING_INTERVAL = 22000;
var LOCALHOST = '127.0.0.1';
var sessionStorage = new LRU({
maxAge: 1000 * 60 * 12,
max: 1600
});
var createServer = http.createServer;
var httpRequest = http.request;
var httpsRequest = https.request;
var formatHeaders = hparser.formatHeaders;
var getRawHeaderNames = hparser.getRawHeaderNames;
var getRawHeaders = hparser.getRawHeaders;
var STATUS_CODES = http.STATUS_CODES || {};
var clientIpKey = Symbol('clientIp');
var clientPortKey = Symbol('clientPort');
var remoteAddrKey = Symbol('remoteAddr');
var remotePortKey = Symbol('remotePort');
var pluginName;
var QUERY_RE = /\?.*$/;
var REQ_ID_RE = /^\d{13,15}-\d{1,5}$/;
var sessionOpts, sessionTimer, sessionPending;
var framesOpts, framesTimer, framesPending;
var customParserOpts, customParserTimer, customParserPending;
var reqCallbacks = {};
var resCallbacks = {};
var parserCallbacks = {};
var framesList = [];
var framesCallbacks = [];
var MAX_LENGTH = 100;
var MAX_BUF_LEN = 1024 * 1024;
var TIMEOUT = 300;
var REQ_INTERVAL = 16;
var pluginOpts, storage, sharedStorage;
var MASK_OPTIONS = { mask: true };
var BINARY_MASK_OPTIONS = { mask: true, binary: true };
var BINARY_OPTIONS = { binary: true };
/* eslint-disable no-undef */
var REQ_ID_KEY = Symbol('reqId');
var SESSION_KEY = Symbol('session');
var FRAME_KEY = Symbol('frame');
var REQ_KEY = Symbol('req');
var CLOSED = Symbol('closed');
var NOT_NAME_RE = /[^\w.-]/;
/* eslint-enable no-undef */
var index = 1000;
var pluginVersion = '';
var noop = common.noop;
var certsCache = new LRU({ max: 256 });
var certsCallbacks = {};
var ctx;
var PLUGIN_HOOK_NAME_HEADER;
var CLIENT_INFO_HEADER;
var PROXY_ID_HEADER;
var debugMode;
var pluginInited;
var writeDevLog;
var addRemoteInfo = function(req, headers) {
headers[CLIENT_INFO_HEADER] = [req[clientIpKey], req[clientPortKey], req[remoteAddrKey], req[remotePortKey]].join();
};
process._handlePforkUncaughtException = function (msg, e) {
msg = [
'From: ' + pluginName + pluginVersion,
'Node: ' + process.version,
'Host: ' + os.hostname(),
'Date: ' + new Date().toLocaleString(),
msg
].join('\n');
pluginInited && debugMode && console.error(msg); // eslint-disable-line
common.writeLogSync('\r\n' + msg + '\r\n');
writeDevLog && writeDevLog('\n' + msg);
if (typeof process.handleUncaughtPluginErrorMessage === 'function') {
return process.handleUncaughtPluginErrorMessage(msg, e);
}
};
var appendTrailers = function (_res, res, newTrailers, req) {
if (res.disableTrailer || res.disableTrailers) {
return;
}
common.addTrailerNames(_res, newTrailers, null, null, req);
common.onResEnd(_res, function () {
var trailers = _res.trailers;
if (
!res.chunkedEncoding ||
(common.isEmptyObject(trailers) && common.isEmptyObject(newTrailers))
) {
return;
}
var rawHeaderNames = _res.rawTrailers
? getRawHeaderNames(_res.rawTrailers)
: {};
if (newTrailers) {
newTrailers = common.lowerCaseify(newTrailers, rawHeaderNames);
if (trailers) {
extend(trailers, newTrailers);
} else {
trailers = newTrailers;
}
}
try {
common.removeIllegalTrailers(trailers);
res.addTrailers(formatHeaders(trailers, rawHeaderNames));
} catch (e) {}
});
};
var requestData = function (options, callback) {
request(options, function (err, body) {
if (err) {
return callback(err);
}
try {
return callback(null, JSON.parse(body));
} catch (e) {
return callback(e);
}
});
};
var setContext = function (req) {
if (ctx) {
req.ctx = ctx;
}
req.localStorage = storage;
req.Storage = Storage;
req.sharedStorage = sharedStorage;
// 原始请求信息 (Original request information)
var oReq = (req.originalReq = {});
var oRes = (req.originalRes = {});
var headers = req.headers;
var sessionInfo = common.getSessionInfo(headers);
if (!sessionInfo) {
var clientIp = headers[common.CLIENT_IP_HEADER];
req.clientIp = net.isIP(clientIp) ? clientIp : LOCALHOST;
req.clientPort = +headers[common.CLIENT_PORT_HEADER] || 0;
return;
}
var fullUrl = sessionInfo.fullUrl;
oReq.url = oReq.fullUrl = req.fullUrl = fullUrl;
oReq.extraUrl = sessionInfo._extraUrl;
oReq.ruleProtocol = sessionInfo._ruleProtocol;
req.isHttps = oReq.isHttps = HTTPS_RE.test(fullUrl);
req._isUpgrade = sessionInfo._isUpgrade === '1';
req.notDecompressed = oReq.notDecompressed = sessionInfo._noDecompress === '1';
req.fromTunnel = oReq.fromTunnel = sessionInfo.fromTunnel === '1';
req.fromComposer = oReq.fromComposer = sessionInfo.fromComposer === '1';
oReq.existsCustomCert = sessionInfo._existsCustomCert == '1';
oReq.isUIRequest = req.isUIRequest = sessionInfo._isUIRequest == '1';
oReq.enableCapture = sessionInfo._enableCapture == '1';
oReq.isFromPlugin = req.fromPlugin = oReq.fromPlugin = sessionInfo.isPluginReq == '1';
req[clientIpKey] = req.clientIp = oReq.clientIp = sessionInfo.clientIp || LOCALHOST;
req[clientPortKey] = req.clientPort = oReq.clientPort = +sessionInfo.clientPort || 0;
req[remoteAddrKey] = oReq.remoteAddress = sessionInfo._remoteAddr || LOCALHOST;
req[remotePortKey] = oReq.remotePort = +sessionInfo._remotePort || 0;
oReq.isHttp2 = oReq.isH2 = !!headers[common.ALPN_PROTOCOL_HEADER];
oReq.relativeUrl = sessionInfo._relativeUrl;
oReq.ruleUrl = sessionInfo._ruleUrl;
oReq.realUrl = sessionInfo._finalUrl || oReq.url;
oReq.sniValue = sessionInfo.sniRuleValue;
oReq.pipeValue = sessionInfo._pipeValue;
oReq.hostValue = sessionInfo.host;
oReq.proxyValue = sessionInfo.proxy;
oReq.pacValue = sessionInfo.pac;
oReq.globalValue = sessionInfo.globalValue;
oReq.servername = oReq.serverName = sessionInfo.serverName;
oReq.commonName = sessionInfo.commonName;
oReq.method = sessionInfo.method || 'GET';
oReq.serverIp = oRes.serverIp = sessionInfo.hostIp;
oReq.statusCode = oRes.statusCode = sessionInfo._statusCode;
oReq.ruleValue = sessionInfo._ruleValue;
oReq.pluginVars = getPluginVars(sessionInfo._pluginVarsValue);
oReq.globalPluginVars = getPluginVars(sessionInfo._globalPluginVarsValue);
var originHost = headers[common.ORIGIN_HOST_HEADER]; // for web ui;
oReq.originHost = common.isString(originHost) ? originHost : '';
var pattern = sessionInfo._rawPattern;
if (pattern && pattern[1] === ',') {
oReq.isRexExp = oReq.isRegExp = pattern[0] === '1';
oReq.pattern = pattern.substring(2);
}
var sniType = sessionInfo._sniType;
var isSNI = !sniType;
if (sniType && sniType === '1') {
isSNI = true;
req.isHttpsServer = true;
}
req.useSNI = oReq.useSNI = req.isSNI = oReq.isSNI = isSNI;
oReq.headers = headers;
var certCacheInfo = sessionInfo.hasCertCache;
oReq.certCacheName = certCacheInfo;
oReq.certCacheTime = 0;
if (certCacheInfo) {
var sepIndex = certCacheInfo.indexOf('+');
if (sepIndex !== -1) {
oReq.certCacheName = certCacheInfo.substring(0, sepIndex);
oReq.certCacheTime = parseInt(certCacheInfo.substring(sepIndex + 1)) || 0;
}
}
return compat.compatHeaders(req, sessionInfo);
};
var initState = function (req, name) {
switch (name) {
case 'pauseSend':
req.curSendState = 'pause';
return;
case 'ignoreSend':
req.curSendState = 'ignore';
return;
case 'pauseReceive':
req.curReceiveState = 'pause';
return;
case 'ignoreReceive':
req.curReceiveState = 'ignore';
return;
}
};
var getFrameId = function () {
++index;
if (index > 9990) {
index = 1000;
}
if (index > 99) {
return Date.now() + '-' + index;
}
if (index > 9) {
return Date.now() + '-0' + index;
}
return Date.now() + '-00' + index;
};
var addFrame = function (frame) {
framesList.push(frame);
if (framesList.length > 720) {
framesList.splice(0, 80);
}
};
var getFrameOpts = function (opts) {
if (!opts) {
return {};
}
if (opts === true) {
return { ignore: true };
}
var result = {};
if (opts.ignore === true) {
result.ignore = true;
}
if (opts.compressed === true) {
result.compressed = true;
}
if (opts.opcode > 0) {
result.opcode = opts.opcode == 1 ? 1 : 2;
}
if (opts.isError) {
result.isError = true;
}
if (typeof opts.charset === 'string') {
result.charset = opts.charset;
}
return result;
};
var pushFrame = function (reqId, data, opts, isClient) {
if (data == null) {
return;
}
if (!Buffer.isBuffer(data)) {
try {
if (typeof data !== 'string') {
data = JSON.stringify(data);
}
data = data && Buffer.from(data);
} catch (e) {
data = null;
}
}
if (!data) {
return;
}
opts = getFrameOpts(opts);
opts.reqId = reqId;
opts.frameId = getFrameId();
opts.isClient = isClient;
opts.length = data.length;
if (opts.length > MAX_BUF_LEN) {
data = data.slice(0, MAX_BUF_LEN);
}
opts.base64 = data.toString('base64');
addFrame(opts);
};
var addParserApi = function (req, conn, state, reqId) {
state = state.split(',').forEach(function (name) {
initState(req, name);
});
req.on('clientFrame', function (data, opts) {
pushFrame(reqId, data, opts, true);
});
req.on('serverFrame', function (data, opts) {
pushFrame(reqId, data, opts);
});
var on = req.on;
req.on = function (eventName) {
on.apply(this, arguments);
var curState, prevState;
if (eventName === 'sendStateChange') {
curState = req.curSendState;
prevState = req.prevSendState;
} else if (eventName === 'receiveStateChange') {
curState = req.curReceiveState;
prevState = req.prevReceiveState;
}
if (curState || prevState) {
req.emit(eventName, curState, prevState);
}
};
var disconnected;
var emitDisconnect = function (err) {
if (disconnected) {
return;
}
req.isDisconnected = disconnected = true;
addFrame({
reqId: reqId,
frameId: getFrameId(),
closed: !err,
err: err && err.message,
bin: ''
});
delete parserCallbacks[reqId];
req.emit('disconnect', err);
};
conn.on('error', emitDisconnect);
conn.on('close', emitDisconnect);
parserCallbacks[reqId] = function (data) {
if (!data) {
return conn.destroy();
}
var sendState, receiveState;
if (data.sendStatus === 1) {
sendState = 'pause';
} else if (data.sendStatus === 2) {
sendState = 'ignore';
}
if (data.receiveStatus === 1) {
receiveState = 'pause';
} else if (data.receiveStatus === 2) {
receiveState = 'ignore';
}
var curSendState = req.curSendState;
if (curSendState != sendState) {
req.prevSendState = req.curSendState;
req.curSendState = sendState;
try {
req.emit('sendStateChange', req.curSendState, req.prevSendState);
} catch (e) {}
}
var curReceiveState = req.curReceiveState;
if (curReceiveState != receiveState) {
req.prevReceiveState = req.curReceiveState;
req.curReceiveState = receiveState;
try {
req.emit(
'receiveStateChange',
req.curReceiveState,
req.prevReceiveState
);
} catch (e) {}
}
if (Array.isArray(data.toClient)) {
data.toClient.forEach(function (frame) {
var buf = base64ToBuffer(frame.base64);
try {
buf && req.emit('sendToClient', buf, frame.binary);
} catch (e) {}
});
}
if (Array.isArray(data.toServer)) {
data.toServer.forEach(function (frame) {
var buf = base64ToBuffer(frame.base64);
try {
buf && req.emit('sendToServer', buf, frame.binary);
} catch (e) {}
});
}
};
retryCustomParser();
};
var addSessionStorage = function (req, id) {
req.sessionStorage = {
set: function (key, value) {
var cache = sessionStorage.get(id);
if (!cache) {
cache = {};
sessionStorage.set(id, cache);
}
cache[key] = value;
return value;
},
get: function (key) {
var cache = sessionStorage.get(id);
return cache && cache[key];
},
remove: function (key) {
var cache = sessionStorage.peek(id);
if (cache) {
delete cache[key];
}
}
};
};
var ADDITIONAL_FIELDS = [
'headers',
'rawHeaders',
'trailers',
'rawTrailers',
'url',
'method',
'statusCode',
'statusMessage',
'sendEstablished',
'unsafe_getReqSession',
'unsafe_getSession',
'unsafe_getFrames',
'getReqSession',
'getSession',
'getFrames',
'request',
'originalReq',
'response',
'originalRes',
'localStorage',
'Storage',
'clientIp',
'sessionStorage'
];
function getPluginVars(value) {
value = base64ToBuffer(value);
if (value) {
try {
value = JSON.parse(value.toString());
if (Array.isArray(value)) {
return value;
}
} catch (e) {}
}
return [];
}
var initReq = function (req, res, isServer) {
if (req.originalReq && req.originalRes) {
return;
}
var destroy = function () {
if (!req._hasError) {
req._hasError = true;
req.destroy && req.destroy();
res.destroy && res.destroy();
}
};
req.on('error', destroy);
res.on('error', destroy);
req.getReqSession = req.unsafe_getReqSession = function (cb) {
return getSession(req, cb, true);
};
req.getSession = req.unsafe_getSession = function (cb) {
return getSession(req, cb);
};
req.getFrames = req.unsafe_getFrames = function (cb) {
return getFrames(req, cb);
};
var sessionInfo = setContext(req) || {};
var oReq = req.originalReq;
var reqId = sessionInfo.reqId;
if (isServer) {
var parseStatus = sessionInfo.customParser;
req.customParser = oReq.customParser = !!parseStatus;
req.customParser && addParserApi(req, res, parseStatus, reqId);
}
req[REQ_ID_KEY] = oReq.id = reqId;
addSessionStorage(req, reqId);
oReq.clientId = String(req.headers[common.CLIENT_ID_HEADER] || '');
};
var getOptions = function (opts, binary, toServer) {
if (opts) {
opts.mask = toServer;
opts.binary = opts.binary || opts.opcode == 2;
return opts;
}
if (toServer) {
return binary ? BINARY_MASK_OPTIONS : MASK_OPTIONS;
}
return binary ? BINARY_OPTIONS : '';
};
var base64ToBuffer = function (base64) {
if (base64) {
try {
return new Buffer(base64, 'base64');
} catch (e) {}
}
};
var getBuffer = function (item) {
return base64ToBuffer(item.base64);
};
var getText = function (item) {
var body = base64ToBuffer(item.base64) || '';
return common.bufferToString(body);
};
var defineProps = function (obj) {
if (!obj) {
return;
}
if (Object.defineProperties) {
Object.defineProperties(obj, {
body: {
get: function () {
return getText(obj);
}
},
buffer: {
get: function () {
return getBuffer(obj);
}
}
});
} else {
obj.body = getText(obj);
obj.buffer = getBuffer(obj);
}
};
var execCallback = function (id, cbs, item) {
var cbList = cbs[id];
if (cbList && (cbs === reqCallbacks || !item || item.endTime)) {
item = item || '';
defineProps(item.req);
defineProps(item.res);
delete cbs[id];
cbList.forEach(function (cb) {
try {
cb(item);
} catch (e) {}
});
}
};
var retryRequestSession = function (time) {
if (!sessionTimer) {
sessionTimer = setTimeout(requestSessions, time || TIMEOUT);
}
};
var requestSessions = function () {
clearTimeout(sessionTimer);
sessionTimer = null;
if (sessionPending) {
return;
}
var reqList = Object.keys(reqCallbacks);
var resList = Object.keys(resCallbacks);
if (!reqList.length && !resList.length) {
return;
}
sessionPending = true;
var _reqList = reqList.slice(0, MAX_LENGTH);
var _resList = resList.slice(0, MAX_LENGTH);
var query =
'?reqList=' + JSON.stringify(_reqList) +
'&resList=' + JSON.stringify(_resList);
sessionOpts.path = sessionOpts.path.replace(QUERY_RE, query);
sessionOpts.search = query;
requestData(sessionOpts, function (err, result) {
sessionPending = false;
if (err || !result) {
return retryRequestSession();
}
Object.keys(result).forEach(function (id) {
var item = result[id];
execCallback(id, reqCallbacks, item);
execCallback(id, resCallbacks, item);
});
retryRequestSession(REQ_INTERVAL);
});
};
var retryRequestFrames = function (time) {
if (!framesTimer) {
framesTimer = setTimeout(requestFrames, time || TIMEOUT);
}
};
var requestFrames = function () {
clearTimeout(framesTimer);
framesTimer = null;
if (framesPending) {
return;
}
var cb = framesCallbacks.shift();
if (!cb) {
return;
}
var req = cb[REQ_KEY];
if (req[CLOSED]) {
return cb('');
}
framesPending = true;
var query =
'?curReqId=' + req[REQ_ID_KEY] + '&lastFrameId=' + (req[FRAME_KEY] || '');
framesOpts.path = framesOpts.path.replace(QUERY_RE, query);
framesOpts.search = query;
requestData(framesOpts, function (err, result) {
framesPending = false;
if (err || !result) {
framesCallbacks.push(cb);
return retryRequestFrames();
}
var frames = result.frames;
var closed;
if (Array.isArray(frames)) {
var last = frames[frames.length - 1];
var frameId = last && last.frameId;
if (frameId) {
req[FRAME_KEY] = frameId;
frames.forEach(defineProps);
closed = !!(last.closed || last.err);
}
} else {
closed = !frames;
}
if (closed || frames.length) {
req[CLOSED] = closed;
try {
cb(frames || '');
} catch (e) {}
} else {
framesCallbacks.push(cb);
}
retryRequestFrames(REQ_INTERVAL);
});
};
var retryCustomParser = function (time) {
if (!customParserTimer) {
customParserTimer = setTimeout(customParser, time || TIMEOUT);
}
};
var customParser = function () {
clearTimeout(customParserTimer);
customParserTimer = null;
if (customParserPending) {
return;
}
var idList = Object.keys(parserCallbacks);
if (!idList.length && !framesList.length) {
return;
}
customParserPending = true;
customParserOpts.body = {
idList: idList,
frames: framesList.splice(0, 10)
};
requestData(customParserOpts, function (err, result) {
customParserPending = false;
customParserOpts.body = undefined;
if (err || !result) {
return retryCustomParser();
}
idList.forEach(function (reqId) {
var cb = parserCallbacks[reqId];
cb && cb(result[reqId]);
});
retryCustomParser(framesList.length > 0 ? 20 : 300);
});
};
function isFrames(item) {
if (/^wss?:\/\//.test(item.url)) {
return item.res.statusCode == 101;
}
return item.inspect || item.useFrames;
}
var getFrames = function (req, cb) {
var reqId = req[REQ_ID_KEY];
if (!REQ_ID_RE.test(reqId) || typeof cb !== 'function') {
return;
}
if (req[CLOSED]) {
return cb('');
}
cb[REQ_KEY] = req;
getSession(req, function (session) {
if (
!session ||
session.reqError ||
session.resError ||
!isFrames(session)
) {
req[CLOSED] = 1;
return cb('');
}
framesCallbacks.push(cb);
requestFrames();
});
};
var getSession = function (req, cb, isReq) {
var reqId = req[REQ_ID_KEY];
if (!REQ_ID_RE.test(reqId) || typeof cb !== 'function') {
return;
}
var session = req[SESSION_KEY];
if (session != null) {
if (isReq) {
return cb(session);
}
if (!session || session.endTime) {
return cb(session);
}
}
var cbList = isReq ? reqCallbacks[reqId] : resCallbacks[reqId];
if (cbList) {
if (cbList.indexOf(cb) === -1) {
cbList.push(cb);
}
} else {
cbList = [
function (s) {
req[SESSION_KEY] = s;
cb(s);
}
];
}
if (isReq) {
reqCallbacks[reqId] = cbList;
} else {
resCallbacks[reqId] = cbList;
}
retryRequestSession();
};
var initWsReq = function (req, res) {
initReq(req, res, true);
};
var initConnectReq = function (req, res) {
if (req.originalReq && req.originalRes) {
return;
}
var established;
initWsReq(req, res);
req.sendEstablished = function (err, cb) {
if (established) {
return;
}
if (typeof err === 'function') {
cb = err;
err = null;
}
req.isEstablished = true;
established = true;
var msg = err ? 'Bad Gateway' : 'Connection Established';
var body = String((err && err.stack) || '');
var length = Buffer.byteLength(body);
var resCtn = [
'HTTP/1.1 ' + (err ? 502 : 200) + ' ' + msg,
'Content-Length: ' + length,
'Proxy-Agent: ' + pluginOpts.shortName
];
if (err || !cb || !req.headers[common.ACK_HEADER]) {
resCtn.push('\r\n', body);
res.write(resCtn.join('\r\n'));
return cb && cb();
}
resCtn.push('x-whistle-allow-tunnel-ack: 1');
resCtn.push('\r\n', body);
res.once('data', function (chunk) {
if (!req._hasError) {
res.pause();
var on = res.on;
res.on = function () {
res.on = on;
res.resume();
return on.apply(this, arguments);
};
chunk.length > 1 && res.unshift(chunk.slice(1));
cb();
}
});
return res.write(resCtn.join('\r\n'));
};
};
var loadModule = function (filepath) {
try {
return require(filepath);
} catch (e) {}
};
function getFunction(fn) {
return typeof fn === 'function' ? fn : null;
}
function notEmptyStr(str) {
return str && typeof str === 'string';
}
function getHookName(req) {
var name = req.headers[PLUGIN_HOOK_NAME_HEADER];
delete req.headers[PLUGIN_HOOK_NAME_HEADER];
return typeof name === 'string' ? name : null;
}
function handleError(socket, sender, receiver) {
var emitError = function (err) {
if (socket._emittedError) {
return;
}
socket._emittedError = true;
socket.emit('error', err);
};
sender && sender.on('error', emitError);
receiver && (receiver.onerror = emitError);
}
var GZIP_RE = /^\s*gzip\s*$/i;
function getCustomBody(body, req, cb) {
var headers = req.headers;
if (body && typeof body.pipe === 'function') {
delete headers['content-length'];
return cb(body);
}
var handleCb = function() {
if (headers['content-length'] != null) {
headers['content-length'] = body.length;
delete headers['transfer-encoding'];
}
cb(body);
};
if (Buffer.isBuffer(body)) {
return handleCb();
}
var encoding = headers['content-encoding'];
if (body && typeof body === 'object' && common.isUrlEncoded(req)) {
body = qs.stringify(body);
}
body = toBuffer(body) || '';
if (!GZIP_RE.test(encoding)) {
return handleCb();
}
gzip(body, function(_, result) {
body = result || body;
handleCb();
});
}
function wrapTunnelWriter(socket, toServer) {
var write = socket.write;
var end = socket.end;
var sender = wsParser.getSender(socket, toServer);
handleError(socket, sender);
socket.write = function (chunk, encoding, cb) {
if ((chunk = toBuffer(chunk))) {
if (encoding === 'binary') {
return write.call(this, chunk, encoding, cb);
}
if (toServer) {
sender.send(chunk, extend({ mask: true }, encoding));
} else {
sender.send(chunk);
}
}
};
if (toServer) {
socket.write = function (chunk, opts, cb) {
if ((chunk = toBuffer(chunk))) {
if (opts === 'binary') {
return write.call(this, chunk, opts, cb);
}
sender.send(chunk, getOptions(opts, false, true));
}
};
socket.writeText = function (chunk) {
if ((chunk = toBuffer(chunk))) {
sender.send(chunk, getOptions(null, false, true));
}
};
socket.writeBin = function (chunk) {
if ((chunk = toBuffer(chunk))) {
sender.send(chunk, getOptions(null, true, true));
}
};
socket.closeWebSocket = function (code, data, cb) {
sender.close(code || 1000, data, true, cb);
};
socket.ping = function (data) {
return sender.ping(data, { mask: true });
};
} else {
socket.write = function (chunk, encoding, cb) {
if ((chunk = toBuffer(chunk))) {
if (encoding === 'binary') {
return write.call(this, chunk, encoding, cb);
}
sender.send(chunk);
}
};
}
socket.end = function (chunk, encoding, cb) {
chunk && socket.write(chunk, encoding, cb);
return end.call(this);
};
return socket;
}
function wrapTunnelReader(socket, fromServer, maxPayload) {
socket.wsExts = '';
var receiver = wsParser.getReceiver(socket, fromServer, maxPayload);
var emit = socket.emit;
handleError(socket, null, receiver);
socket.emit = function (type, chunk) {
if (type === 'data' && chunk) {
return receiver.add(chunk);
}
return emit.apply(this, arguments);
};
receiver.onData = function (chunk, opts) {
emit.call(socket, 'data', chunk, opts);
};
receiver.onpong = function (data, opts) {
socket.emit('pong', data, opts);
};
return socket;
}
function getReqRules(opts, reqRules) {
var rules = opts && opts.rules;
if (!notEmptyStr(rules)) {
return reqRules;
}
return reqRules ? rules + '\n' + reqRules : rules;
}
function setReqRules(uri, reqRules) {
if (!reqRules) {
return;
}
var rules = formatRules(reqRules);
var values = reqRules.values;
if (!rules) {
return;
}
uri.headers = uri.headers || {};
uri.headers['x-whistle-rule-value'] = encodeURIComponent(rules);
if (!values) {
return;
}
if (typeof values !== 'string') {
try {
values = JSON.stringify(values);
} catch (e) {
return;
}
}
uri.headers['x-whistle-key-value'] = encodeURIComponent(values);
}
function addFrameHandler(req, socket, maxWsPayload, fromClient, toServer) {
socket.wsExts = req.headers['sec-websocket-extensions'] || '';
var receiver = wsParser.getReceiver(socket, !fromClient, maxWsPayload, req.notDecompressed);
var emit = socket.emit;
var write = socket.write;
var end = socket.end;
var lastOpts;
var sender = wsParser.getSender(socket, toServer);
handleError(socket, sender, receiver);
socket.emit = function (type, chunk) {
if (type === 'data' && chunk) {
return receiver.add(chunk);
}
return emit.apply(this, arguments);
};
receiver.onData = function (chunk, opts) {
if (opts && opts.opcode == 2) {
opts.binary = true;
}
lastOpts = opts;
emit.call(socket, 'data', chunk, opts);
};
socket.write = function (chunk, opts, cb) {
if ((chunk = toBuffer(chunk))) {
if (opts === 'binary') {
return write.call(this, chunk, opts, cb);
}
sender.send(chunk, getOptions(opts || lastOpts, false, toServer));
}
};
socket.writeText = function (chunk) {
if ((chunk = toBuffer(chunk))) {
sender.send(chunk, getOptions(null, false, toServer));
}
};
socket.writeBin = function (chunk) {
if ((chunk = toBuffer(chunk))) {
sender.send(chunk, getOptions(null, true, toServer));
}
};
socket.end = function (chunk, opts, cb) {
chunk && socket.write(chunk, opts, cb);
return end.call(this);
};
if (fromClient === toServer) {
return handleWsSignal(receiver, sender);
}
return {
receiver: receiver,
sender: sender
};
}
function formatRawHeaders(headers, req) {
var rawHeaders = headers && (req.rawHeaders || req);
if (!Array.isArray(rawHeaders)) {
return headers;
}
var rawNames = getRawHeaderNames(rawHeaders);
if (headers.trailer && !rawNames.trailer) {
rawNames.trailer = 'Trailer';
}
return formatHeaders(headers, rawNames);
}
function addErrorHandler(req, client) {
var done;
client.on('error', function (err) {
if (!done) {
done = true;
req.destroy && req.destroy(err);
client.abort && client.abort();
}
});
}
function handleWsSignal(receiver, sender) {
receiver.onping = sender.ping.bind(sender);
receiver.onpong = sender.pong.bind(sender);
receiver.onclose = function (code, message, opts) {
sender.close(code, message, opts.masked);
};
}
function destroySocket() {
this.destroy();
}
function formatRules(rules) {
if (Array.isArray(rules)) {
return rules.join('\n');
}
rules = rules.rules || rules;
return typeof rules === 'string' ? rules : undefined;
}
function isBodyData(result) {
return typeof result === 'string' || Buffer.isBuffer(result) || typeof result.pipe === 'function';
}
module.exports = async function (options, callback) {
var root = options.value;
options.CLIENT_ID_HEADER = common.CLIENT_ID_HEADER;
options.extractSaz = extractSaz;
options.generateSaz = generateSaz;
options.zipBody = getCustomBody;
if (options.isDev) {
var devLogFile = path.join(root, '.console.log');
var devLogOptions = { flag: 'a+' };
writeDevLog = function(log) {
fs.writeFile(devLogFile, log, devLogOptions, common.noop);
};
LEVELS.forEach(function (level) {
var originalFn = console[level]; // eslint-disable-line
if (originalFn) {
console[level] = function() { // eslint-disable-line
try {
writeDevLog(format.apply(null, arguments) + '\n');
} catch (e) {}
return originalFn.apply(this, arguments);
};
}
});
}
options.Storage = Storage;
options.parseUrl = parseUrl;
options.formatHeaders = formatRawHeaders;
options.wsParser = wsParser;
pluginVersion = '@' + options.version;
var wrapWsReader = function (socket, maxPayload) {
wrapTunnelReader(socket, true, maxPayload);
return socket;
};
var wrapWsWriter = function (socket) {
wrapTunnelWriter(socket, true);
var timer = setInterval(function () {
socket.ping();
}, PING_INTERVAL);
var handleClose = function () {
if (timer) {
clearInterval(timer);
timer = null;
}
};
socket.once('error', handleClose);
socket.once('close', handleClose);
socket.stopPing = function () {
timer && clearInterval(timer);
};
socket.startPing = function () {
if (timer) {
socket.ping();
timer = setInterval(function () {
socket.ping();
}, PING_INTERVAL);
}
};
return socket;
};
options.wrapWsReader = wrapWsReader;
options.wrapWsWriter = wrapWsWriter;
options.require = rootRequire;
options.whistleRequire = rootRequire;
options.whistleRequirePath = rootRequire.resolve('./require');
debugMode = options.debugMode;
pluginName = options.name;
var config = options.config;
var boundIp = config.host;
var boundPort = config.port;
var PLUGIN_HOOKS = config.PLUGIN_HOOKS;
var RES_RULES_HEAD = config.RES_RULES_HEAD;
var SHOW_LOGIN_BOX = config.SHOW_LOGIN_BOX;
var setResRules = function(headers, obj) {
var rules = formatRules(obj);
if (!rules) {
return;
}
obj = JSON.stringify({
rules: rules,
values: obj.values,
root: root
});
headers[RES_RULES_HEAD] = encodeURIComponent(obj);
};
CLIENT_INFO_HEADER = config.CLIENT_INFO_HEADER;
PROXY_ID_HEADER = config.PROXY_ID_HEADER;
options.getTempFilePath = function(filePath) {
if (common.TEMP_PATH_RE.test(filePath)) {
return path.join(config.TEMP_FILES_PATH, RegExp.$1);
}
};
delete config.PLUGIN_HOOKS;
delete config.PROXY_ID_HEADER;
delete config.CLIENT_INFO_HEADER;
delete config.RES_RULES_HEAD;
delete config.SHOW_LOGIN_BOX;
PLUGIN_HOOK_NAME_HEADER = config.PLUGIN_HOOK_NAME_HEADER;
options.shortName = pluginName.substring(pluginName.indexOf('/') + 1);
var sharedFilePath = path.join(config.baseDir, '.shared_storage', options.name + '.txt');
var pluginDataDir = (config.pluginBaseDir = path.join(
config.baseDir,
'.plugins',
options.name
));
if (config.storage) {
pluginDataDir += encodeURIComponent('/' + config.storage);
}
config.pluginDataDir = pluginDataDir;
storage = new Storage(pluginDataDir);
options.storage = options.localStorage = storage;
sharedStorage = getSharedStorage(sharedFilePath);
options.sharedStorage = sharedStorage;
pluginOpts = options;
var authKey = config.authKey;
delete config.authKey;
var headers = {
'x-whistle-auth-key': authKey,
'content-type': 'application/json'
};
var baseUrl = 'http://' + common.joinIpPort(boundIp, boundPort);
options.baseUrl = baseUrl;
baseUrl += '/cgi-bin/';
sessionOpts = parseUrl(baseUrl + 'get-session?');
sessionOpts.headers = headers;
framesOpts = parseUrl(baseUrl + 'get-frames?');
framesOpts.headers = headers;
customParserOpts = parseUrl(baseUrl + 'custom-frames?');
customParserOpts.headers = headers;
customParserOpts.method = 'POST';
var normalizeArgs = function (
uri,
cb,
req,
curUrl,
isWs,
opts,
alpnProtocol
) {
var type = uri && typeof uri;
var headers, method, body;
if (type !== 'string') {
if (type === 'object') {
if (uri.headers) {
headers = extend({}, uri.headers);
}
method = typeof uri.method === 'string' ? uri.method : null;
opts = opts || uri;
body = common.hasRequestBody(req) ? toBuffer(uri.body || uri.data) : null;
uri = uri.uri || uri.url || uri.href || curUrl;
} else {
if (type === 'function') {
opts = cb;
cb = uri;
} else if (cb && typeof cb !== 'function') {
opts = cb;
cb = null;
}
uri = curUrl;
}
}
uri = parseUrl(uri);
headers = headers || req.headers;
if (isWs) {
headers.upgrade = headers.upgrade || 'websocket';
headers.connection = 'Upgrade';
uri.method = 'GET';
} else {
uri.method = method || req.method;
}
var isHttps = uri.protocol === 'https:' || uri.protocol === 'wss:';
headers.host = uri.host;
uri.protocol = null;
if (!uri.rejectUnauthorized) {
uri.rejectUnauthorized = false;
}
if (!opts || !notEmptyStr(opts.host)) {
headers[PROXY_ID_HEADER] = 1;
uri.host = boundIp;
uri.port = boundPort;
addRemoteInfo(req, headers);
if (isHttps) {
headers[common.HTTPS_FIELD] = 1;
if (alpnProtocol) {
headers[common.ALPN_PROTOCOL_HEADER] = alpnProtocol;
}
}
isHttps = false;
} else {
uri.host = opts.host;
uri.port = opts.port > 0 ? opts.port : isHttps ? 443 : 80;
if (isHttps) {
if (opts.internalRequest) {
isHttps = false;
headers[common.HTTPS_FIELD] = 1;
} else {
uri.protocol = 'https:';
}
}
}
delete uri.hostname;
uri.headers = formatRawHeaders(headers, req);
uri.agent = false;
return [uri, cb, isHttps, opts, body];
};
var authPort,
sniPort,
port,
statsPort,
resStatsPort,
uiPort,
rulesPort,
resRulesPort,
tunnelRulesPort,
tunnelPort;
var reqWritePort, reqReadPort, resWritePort, resReadPort;
var wsReqWritePort, wsReqReadPort, wsResWritePort, wsResReadPort;
var tunnelReqWritePort,
tunnelReqReadPort,
tunnelResWritePort,
tunnelResReadPort;
var upgrade;
var callbackHandler = function () {
pluginInited = true;
callback(null, {
authPort,
sniPort,
port: port,
upgrade: upgrade,
statsPort: statsPort,
resStatsPort: resStatsPort,
uiPort: uiPort,
rulesPort: rulesPort,
resRulesPort: resRulesPort,
tunnelRulesPort: tunnelRulesPort,
tunnelPort: tunnelPort,
reqWritePort: reqWritePort,
reqReadPort: reqReadPort,
resWritePort: resWritePort,
resReadPort: resReadPort,
wsReqWritePort: wsReqWritePort,
wsReqReadPort: wsReqReadPort,
wsResWritePort: wsResWritePort,
wsResReadPort: wsResReadPort,
tunnelReqWritePort: tunnelReqWritePort,
tunnelReqReadPort: tunnelReqReadPort,
tunnelResWritePort: tunnelResWritePort,
tunnelResReadPort: tunnelResReadPort
});
};
try {
require.resolve(options.value);
} catch (e) {
return callbackHandler();
}
options.LRU = LRU;
var cgiHeaders = {};
cgiHeaders[PROXY_ID_HEADER] = options.shortName.substring(8);
cgiHeaders['x-whistle-auth-key'] = authKey;
var parseCgiUrl = function (url) {
var opts = parseUrl(url);
opts.headers = cgiHeaders;
return opts;
};
var topOpts = parseCgiUrl(baseUrl + 'top');
var rulesUrlOpts = parseCgiUrl(baseUrl + 'rules/list2');
var valuesUrlOpts = parseCgiUrl(baseUrl + 'values/list2');
var rulesListUrlOpts = parseCgiUrl(baseUrl + 'rules/list2?order=1');
var valuesListUrlOpts = parseCgiUrl(baseUrl + 'values/list2?order=1');
var pluginsOpts = parseCgiUrl(baseUrl + 'plugins/get-plugins?');
var composeOpts = parseCgiUrl(baseUrl + 'composer');
var certsInfoUrlOpts = parseCgiUrl(baseUrl + 'get-custom-certs-info');
var enableOpts = parseCgiUrl(baseUrl + 'plugins/is-enable');
var updateRulesOpts = parseCgiUrl(baseUrl + 'plugins/update-rules');
var httpsStatusOpts = parseCgiUrl(baseUrl + 'https-status');
composeOpts.method = 'POST';
composeOpts.headers['Content-Type'] = 'application/json';
var requestCgi = function (opts, cb) {
if (typeof cb !== 'function') {
return;
}
request(opts, function (err, body, res) {
if (body && res.statusCode == 200) {
try {
return cb(JSON.parse(body) || '', res);
} catch (e) {}
}
cb('', res, err);
});
};
var getValue = function (key, cb) {
if (typeof cb !== 'function') {
return;
}
if (!key || typeof key !== 'string') {
return cb();
}
var valueOpts = parseCgiUrl(
baseUrl + 'values/value?key=' + encodeURIComponent(key)
);
requestCgi(valueOpts, function (data) {
if (!data) {
return getValue(key, cb);
}
cb(data.value);
});
};
var getCert = function (domain, callback) {
if (!domain || typeof domain !== 'string') {
return callback('');
}
var index = domain.indexOf(':');
if (index !== -1) {
domain = domain.substring(0, index);
if (!domain) {
return callback('');
}
}
if (domain !== 'rootCA') {
domain = domain.toLowerCase();
}
var cert = certsCache.get(domain);
if (cert) {
return callback(cert);
}
var cbs = certsCallbacks[domain];
if (cbs) {
return cbs.push(callback);
}
cbs = [callback];
certsCallbacks[domain] = cbs;
var opts = parseCgiUrl(baseUrl + 'get-cert?domain=' + domain);
requestCgi(opts, function (cert) {
cert && certsCache.set(domain, cert);
delete certsCallbacks[domain];
cbs.forEach(function (cb) {
cb(cert);
});
});
};
options.getValue = getValue;
options.getCert = getCert;
options.getRootCA = function (callback) {
return getCert('rootCA', callback);
};
options.getHttpsStatus = function (callback) {
requestCgi(httpsStatusOpts, callback);
};
options.getTop =
options.getRuntimeInfo =
options.getProcessData =
options.getPerfData =
function (callback) {
requestCgi(topOpts, callback);
};
var waitingUpdateRules;
var updateTimer;
var updateRules = function () {
requestCgi(updateRulesOpts, function(data) {
if (!data) {
if (updateTimer) {
clearTimeout(updateTimer);
updateTimer = null;
waitingUpdateRules = false;
}
requestCgi(updateRulesOpts, noop);
}
});
if (waitingUpdateRules) {
waitingUpdateRules = false;
updateTimer = setTimeout(updateRules, 300);
} else {
updateTimer = null;
}
};
options.updateRules = function () {
if (updateTimer) {
waitingUpdateRules = true;
} else {
updateTimer = setTimeout(updateRules, 600);
}
};
options.composer = options.compose = function (data, cb) {
var needResponse = typeof cb === 'function';
if (!data) {
return needResponse ? cb() : '';
}
data.needResponse = needResponse;
if (data.headers && typeof data.headers !== 'string') {
data.headers = JSON.stringify(data.headers);
}
if (data.base64) {
delete data.body;
if (Buffer.isBuffer(data.base64)) {
data.base64 = data.base64.toString('base64');
}
} else if (Buffer.isBuffer(data.body)) {
data.base64 = data.body.toString('base64');
delete data.body;
}
composeOpts.body = data;
request(composeOpts, function (err, body, res) {
if (!needResponse) {
return;
}
if (!err && res.statusCode == 200) {
try {
return cb(err, JSON.parse(body));
} catch (e) {}
}
cb(err || new Error(res.statusCode || 'unknown'));
});
};
function getData(cb, isList, opt1, opt2) {
if (typeof cb !== 'function') {
var temp = cb;
cb = isList;
isList = temp;
}
requestCgi(isList === true ? opt1 : opt2, cb);
}
options.getRules = function (cb, isList) {
getData(cb, isList, rulesListUrlOpts, rulesUrlOpts);
};
options.getValues = function (cb, isList) {
getData(cb, isList, valuesListUrlOpts, valuesUrlOpts);
};
options.getPlugins = function(cb) {
requestCgi(pluginsOpts, cb);
};
var certsInfo;
options.getCustomCertsInfo = function (cb) {
if (typeof cb !== 'function') {
return;
}
if (certsInfo) {
return cb(certsInfo);
}
requestCgi(certsInfoUrlOpts, function (data) {
certsInfo = certsInfo || data;
cb(certsInfo);
});
};
var enableCallbacks = [];
options.isEnable = options.isActive = function (cb) {
if (typeof cb !== 'function') {
return;
}
enableCallbacks.push(cb);
if (enableCallbacks.length > 1) {
return;
}
var checkEnable = function () {
requestCgi(enableOpts, function (data, r) {
if (!data) {
return setTimeout(checkEnable, 600);
}
enableCallbacks.forEach(function (callback) {
callback(data.enable);
});
enableCallbacks = [];
});
};
checkEnable();
};
var initProxy = function(cb) {
getProxy({
PROXY_ID_HEADER: PROXY_ID_HEADER,
proxyIp: boundIp,
proxyPort: boundPort,
pluginName: pluginName,
wrapWsReader: wrapWsReader,
wrapWsWriter: wrapWsWriter
}, cb);
};
var initServers = function (_ctx) {
ctx = _ctx || ctx;
var execPlugin = require(options.value) || '';
var execAuth =
getFunction(execPlugin.auth) || getFunction(execPlugin.verify);
var sniCallback =
getFunction(execPlugin.sniCallback) ||
getFunction(execPlugin.SNICallback);
var startServer = getFunction(
execPlugin.pluginServer || execPlugin.server || execPlugin
);
var startStatsServer = getFunction(
execPlugin.statServer ||
execPlugin.statsServer ||
execPlugin.reqStatServer ||
execPlugin.reqStatsServer
);
var startResStatsServer = getFunction(
execPlugin.resStatServer || execPlugin.resStatsServer
);
var startUIServer = getFunction(
execPlugin.uiServer || execPlugin.innerServer || execPlugin.internalServer
);
var startRulesServer = getFunction(
execPlugin.pluginRulesServer ||
execPlugin.rulesServer ||
execPlugin.reqRulesServer
);
var startResRulesServer = getFunction(execPlugin.resRulesServer);
var startTunnelRulesServer = getFunction(
execPlugin.pluginRulesServer || execPlugin.tunnelRulesServer
);
var startTunnelServer =
getFunction(
execPlugin.pluginServer ||
execPlugin.tunnelServer ||
execPlugin.connectServer
) || startServer;
var startReqRead = getFunction(
execPlugin.reqRead || execPlugin.reqReadServer
);
var startReqWrite = getFunction(
execPlugin.reqWrite || execPlugin.reqWriteServer
);
var startResRead = getFunction(
execPlugin.resRead || execPlugin.resReadServer
);
var startResWrite = getFunction(
execPlugin.resWrite || execPlugin.resWriteServer
);
var startWsReqRead = getFunction(
execPlugin.wsReqRead || execPlugin.wsReqReadServer
);
var startWsReqWrite = getFunction(
execPlugin.wsReqWrite || execPlugin.wsReqWriteServer
);
var startWsResRead = getFunction(
execPlugin.wsResRead || execPlugin.wsResReadServer
);
var startWsResWrite = getFunction(
execPlugin.wsResWrite || execPlugin.wsResWriteServer
);
var startTunnelReqRead = getFunction(
execPlugin.tunnelReqRead || execPlugin.tunnelReqReadServer
);
var startTunnelReqWrite = getFunction(
execPlugin.tunnelReqWrite || execPlugin.tunnelReqWriteServer
);
var startTunnelResRead = getFunction(
execPlugin.tunnelResRead || execPlugin.tunnelResReadServer
);
var startTunnelResWrite = getFunction(
execPlugin.tunnelResWrite || execPlugin.tunnelResWriteServer
);
var staticDir = !startUIServer && options.staticDir;
if (staticDir) {
var app = express();
startUIServer = function (server) {
app.use(
express.static(path.join(options.value, staticDir), {
maxAge: 60000
})
);
server.on('request', app);
};
}
var hasServer =
execAuth ||
sniCallback ||
startServer ||
startStatsServer ||
startResStatsServer ||
startUIServer ||
startRulesServer ||
startResRulesServer ||
startTunnelRulesServer ||
startTunnelServer ||
startReqRead ||
startReqWrite ||
startResRead ||
startResWrite ||
startWsReqRead ||
startWsReqWrite ||
startWsResRead ||
startWsResWrite ||
startTunnelReqRead ||
startTunnelReqWrite ||
startTunnelResRead ||
startTunnelResWrite;
if (!hasServer) {
return callbackHandler();
}
getServer(async function (server, _port) {
var maxWsPayload;
var authServer,
sniServer,
uiServer,
httpServer,
statsServer,
resStatsServer;
var rulesServer, resRulesServer, tunnelRulesServer, tunnelServer;
var reqRead, reqWrite, resWrite, resRead;
var wsReqRead, wsReqWrite, wsResWrite, wsResRead;
var tunnelReqRead, tunnelReqWrite, tunnelResWrite, tunnelResRead;
var setMaxWsPayload = function (payload) {
maxWsPayload = parseInt(payload, 10) || 0;
};
options.ctx = ctx;
if (startUIServer) {
uiServer = createServer();
await startUIServer(uiServer, options);
uiPort = _port;
}
var transferError = function (req, res) {
res.once('error', function (err) {
req.emit('error', err);
});
res.once('close', function (err) {
req.emit('close', err);
});
};
if (execAuth) {
authServer = createServer();
authServer.on('request', async function (req, res) {
initReq(req, res);
transferError(req, res);
var customHeaders = {};
var htmlBody, htmlUrl, location;
var setHeader = function (key, value) {
if (
!notEmptyStr(key) ||
NOT_NAME_RE.test(key) ||
typeof value !== 'string'
) {
return;
}
key = key.toLowerCase();
if (
key.indexOf('x-whistle-') === 0 ||
key === 'proxy-authorization'
) {
customHeaders[key] = value;
}
};
req.setHtml = function (html) {
if (!html || html == null) {
htmlBody = null;
} else if (typeof html === 'string' || Buffer.isBuffer(html)) {
htmlBody = html;
location = null;
htmlUrl = null;
}
};
req.setUrl = req.setFile = function (url) {
if (!url || /\s/.test(url)) {
htmlUrl = null;
} else {
htmlUrl = url;
location = null;
htmlBody = null;
req.showLoginBox = false;
}
};
req.set = req.setHeader = setHeader;
req.setRedirect = func