mirror of
https://github.com/avwo/whistle.git
synced 2026-10-18 06:30:45 +08:00
2643 lines
78 KiB
JavaScript
2643 lines
78 KiB
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 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.onReadEnd(_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.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 Buffer.from(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 setupConnect = function (req, socket, isServer) {
|
|
if (req.originalReq && req.originalRes) {
|
|
return;
|
|
}
|
|
var established;
|
|
initReq(req, socket, isServer);
|
|
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);
|
|
socket.write(resCtn.join('\r\n'));
|
|
return cb && cb();
|
|
}
|
|
resCtn.push('x-whistle-allow-tunnel-ack: 1');
|
|
resCtn.push('\r\n', body);
|
|
socket.once('data', function (chunk) {
|
|
if (!req._hasError) {
|
|
socket.pause();
|
|
var on = socket.on;
|
|
socket.on = function () {
|
|
socket.on = on;
|
|
socket.resume();
|
|
return on.apply(this, arguments);
|
|
};
|
|
chunk.length > 1 && socket.unshift(chunk.slice(1));
|
|
cb();
|
|
}
|
|
});
|
|
return socket.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);
|
|
var notNeedDrain = function() {
|
|
return socket.writableNeedDrain !== true;
|
|
};
|
|
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);
|
|
}
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
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));
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
socket.writeText = function (chunk) {
|
|
if ((chunk = toBuffer(chunk))) {
|
|
sender.send(chunk, getOptions(null, false, true));
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
socket.writeBin = function (chunk) {
|
|
if ((chunk = toBuffer(chunk))) {
|
|
sender.send(chunk, getOptions(null, true, true));
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
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);
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
}
|
|
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);
|
|
var notNeedDrain = function() {
|
|
return socket.writableNeedDrain !== true;
|
|
};
|
|
|
|
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));
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
socket.writeText = function (chunk) {
|
|
if ((chunk = toBuffer(chunk))) {
|
|
sender.send(chunk, getOptions(null, false, toServer));
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
socket.writeBin = function (chunk) {
|
|
if ((chunk = toBuffer(chunk))) {
|
|
sender.send(chunk, getOptions(null, true, toServer));
|
|
return notNeedDrain();
|
|
}
|
|
};
|
|
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 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(common.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 = function (url) {
|
|
if (!url) {
|
|
location = null;
|
|
} else if (common.isUrl(url)) {
|
|
location = url;
|
|
htmlBody = null;
|
|
htmlUrl = null;
|
|
req.showLoginBox = false;
|
|
}
|
|
};
|
|
req.setLogin = function (login) {
|
|
req.showLoginBox = login !== false;
|
|
if (req.showLoginBox) {
|
|
location = null;
|
|
htmlUrl = null;
|
|
}
|
|
};
|
|
try {
|
|
var result = await execAuth(req, options);
|
|
if (req.showLoginBox) {
|
|
customHeaders[SHOW_LOGIN_BOX] = '1';
|
|
}
|
|
if (result === false) {
|
|
customHeaders[common.AUTH_STATUS] = '1';
|
|
if (location) {
|
|
customHeaders.location = location;
|
|
} else if (htmlUrl) {
|
|
customHeaders[common.AUTH_URL] = encodeURIComponent(htmlUrl);
|
|
} else if (typeof htmlBody === 'string') {
|
|
htmlBody = Buffer.from(htmlBody);
|
|
if (htmlBody.length > MAX_BODY_SIZE) {
|
|
htmlBody = htmlBody.slice(0, MAX_BODY_SIZE);
|
|
}
|
|
}
|
|
htmlBody = htmlBody || 'Forbidden';
|
|
} else {
|
|
htmlBody = null;
|
|
}
|
|
res.writeHead(200, customHeaders);
|
|
res.end(htmlBody);
|
|
} catch (e) {
|
|
res.end(
|
|
e && e.message && typeof e.message === 'string'
|
|
? e.message
|
|
: 'Error'
|
|
);
|
|
}
|
|
htmlBody = null;
|
|
});
|
|
authPort = _port;
|
|
}
|
|
|
|
if (sniCallback) {
|
|
sniServer = createServer();
|
|
sniServer.on('request', async function (req, res) {
|
|
initReq(req, res);
|
|
transferError(req, res);
|
|
try {
|
|
var result = await sniCallback(req, options);
|
|
if (result === false || result === true) {
|
|
res.end(result + '');
|
|
} else if (
|
|
result &&
|
|
notEmptyStr(result.key) &&
|
|
notEmptyStr(result.cert)
|
|
) {
|
|
res.end(
|
|
JSON.stringify({
|
|
key: result.key,
|
|
cert: result.cert,
|
|
mtime: result.mtime > 0 ? result.mtime : undefined,
|
|
name: pluginName
|
|
})
|
|
);
|
|
} else {
|
|
res.end();
|
|
}
|
|
} catch (e) {
|
|
res.end(
|
|
e && e.message && typeof e.message === 'string'
|
|
? e.message
|
|
: 'Error'
|
|
);
|
|
}
|
|
});
|
|
sniPort = _port;
|
|
}
|
|
|
|
if (startServer) {
|
|
httpServer = createServer();
|
|
await startServer(httpServer, options);
|
|
upgrade = listenerCount(httpServer, 'upgrade');
|
|
port = _port;
|
|
}
|
|
|
|
if (startStatsServer) {
|
|
statsServer = createServer();
|
|
await startStatsServer(statsServer, options);
|
|
statsPort = _port;
|
|
}
|
|
|
|
if (startResStatsServer) {
|
|
resStatsServer = createServer();
|
|
await startResStatsServer(resStatsServer, options);
|
|
resStatsPort = _port;
|
|
}
|
|
|
|
if (startRulesServer) {
|
|
rulesServer = createServer();
|
|
await startRulesServer(rulesServer, options);
|
|
rulesPort = _port;
|
|
}
|
|
|
|
if (startResRulesServer) {
|
|
resRulesServer = createServer();
|
|
await startResRulesServer(resRulesServer, options);
|
|
resRulesPort = _port;
|
|
}
|
|
|
|
if (startTunnelRulesServer) {
|
|
if (startTunnelRulesServer === startRulesServer) {
|
|
tunnelRulesServer = rulesServer;
|
|
} else {
|
|
tunnelRulesServer = createServer();
|
|
await startTunnelRulesServer(tunnelRulesServer, options);
|
|
}
|
|
tunnelRulesPort = _port;
|
|
}
|
|
|
|
if (startTunnelServer) {
|
|
if (startTunnelServer === startServer) {
|
|
if (listenerCount(httpServer, 'connect')) {
|
|
tunnelServer = httpServer;
|
|
tunnelPort = _port;
|
|
}
|
|
} else {
|
|
tunnelServer = createServer();
|
|
await startTunnelServer(tunnelServer, options);
|
|
tunnelPort = _port;
|
|
}
|
|
}
|
|
|
|
if (startReqRead) {
|
|
reqRead = createServer();
|
|
await startReqRead(reqRead, options);
|
|
reqReadPort = _port;
|
|
}
|
|
|
|
if (startReqWrite) {
|
|
reqWrite = createServer();
|
|
await startReqWrite(reqWrite, options);
|
|
reqWritePort = _port;
|
|
}
|
|
|
|
if (startResRead) {
|
|
resRead = createServer();
|
|
await startResRead(resRead, options);
|
|
resReadPort = _port;
|
|
}
|
|
|
|
if (startResWrite) {
|
|
resWrite = createServer();
|
|
await startResWrite(resWrite, options);
|
|
resWritePort = _port;
|
|
}
|
|
|
|
if (startWsReqRead) {
|
|
wsReqRead = createServer();
|
|
wsReqRead.setMaxWsPayload = setMaxWsPayload;
|
|
await startWsReqRead(wsReqRead, options);
|
|
wsReqReadPort = _port;
|
|
}
|
|
|
|
if (startWsReqWrite) {
|
|
wsReqWrite = createServer();
|
|
wsReqWrite.setMaxWsPayload = setMaxWsPayload;
|
|
await startWsReqWrite(wsReqWrite, options);
|
|
wsReqWritePort = _port;
|
|
}
|
|
|
|
if (startWsResRead) {
|
|
wsResRead = createServer();
|
|
wsResRead.setMaxWsPayload = setMaxWsPayload;
|
|
await startWsResRead(wsResRead, options);
|
|
wsResReadPort = _port;
|
|
}
|
|
|
|
if (startWsResWrite) {
|
|
wsResWrite = createServer();
|
|
wsResWrite.setMaxWsPayload = setMaxWsPayload;
|
|
await startWsResWrite(wsResWrite, options);
|
|
wsResWritePort = _port;
|
|
}
|
|
|
|
if (startTunnelReqRead) {
|
|
tunnelReqRead = createServer();
|
|
await startTunnelReqRead(tunnelReqRead, options);
|
|
tunnelReqReadPort = _port;
|
|
}
|
|
|
|
if (startTunnelReqWrite) {
|
|
tunnelReqWrite = createServer();
|
|
await startTunnelReqWrite(tunnelReqWrite, options);
|
|
tunnelReqWritePort = _port;
|
|
}
|
|
|
|
if (startTunnelResRead) {
|
|
tunnelResRead = createServer();
|
|
await startTunnelResRead(tunnelResRead, options);
|
|
tunnelResReadPort = _port;
|
|
}
|
|
|
|
if (startTunnelResWrite) {
|
|
tunnelResWrite = createServer();
|
|
await startTunnelResWrite(tunnelResWrite, options);
|
|
tunnelResWritePort = _port;
|
|
}
|
|
server.on('request', function (req, res) {
|
|
delete req.headers['x-whistle-internal-id'];
|
|
switch (getHookName(req)) {
|
|
case PLUGIN_HOOKS.AUTH:
|
|
if (authServer) {
|
|
authServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.SNI:
|
|
if (sniServer) {
|
|
sniServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.UI:
|
|
if (uiServer) {
|
|
setContext(req);
|
|
uiServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.HTTP:
|
|
if (httpServer) {
|
|
initReq(req, res, true);
|
|
var alpnProtocol = req.headers[common.ALPN_PROTOCOL_HEADER];
|
|
var curUrl = req.originalReq.realUrl;
|
|
var svrRes = '';
|
|
var reqRules, resRules;
|
|
var writeHead = res.writeHead;
|
|
req.setRules = req.setReqRules = res.setReqRules = function (rules) {
|
|
reqRules = rules;
|
|
return true;
|
|
};
|
|
res.setRules = req.setResRules = res.setResRules = function (rules) {
|
|
resRules = rules;
|
|
return true;
|
|
};
|
|
req.writeHead = res.writeHead = function (code, msg, headers) {
|
|
code = code > 0 ? code : svrRes.statusCode || 101;
|
|
if (msg && typeof msg !== 'string') {
|
|
headers = headers || msg;
|
|
msg = null;
|
|
}
|
|
msg = msg || STATUS_CODES[code] || '';
|
|
headers = headers || svrRes.headers;
|
|
if (resRules) {
|
|
headers = headers || {};
|
|
setResRules(headers, resRules);
|
|
res.setRules = req.setResRules = res.setResRules = noop;
|
|
resRules = null;
|
|
}
|
|
headers = formatRawHeaders(headers, svrRes);
|
|
writeHead.call(res, code, msg, headers);
|
|
res.emit('_wroteHead');
|
|
};
|
|
req.request = function (uri, cb, opts) {
|
|
var args = normalizeArgs(
|
|
uri,
|
|
cb,
|
|
req,
|
|
curUrl,
|
|
false,
|
|
opts,
|
|
alpnProtocol
|
|
);
|
|
uri = args[0];
|
|
cb = args[1];
|
|
opts = args[3];
|
|
var reqBuff = args[4];
|
|
req.setRules = req.setReqRules = res.setReqRules = noop;
|
|
setReqRules(uri, getReqRules(opts, reqRules));
|
|
reqRules = null;
|
|
if (reqBuff && (!uri.headers || uri.headers['content-length'] != null)) {
|
|
uri.headers = uri.headers || {};
|
|
uri.headers['content-length'] = reqBuff.length;
|
|
}
|
|
var request = args[2] ? httpsRequest : httpRequest;
|
|
var client = request(uri, function (_res) {
|
|
svrRes = _res;
|
|
res.once('_wroteHead', function () {
|
|
appendTrailers(_res, res, opts && opts.trailers, req);
|
|
});
|
|
if (typeof cb === 'function') {
|
|
cb.call(this, _res);
|
|
} else {
|
|
res.writeHead();
|
|
_res.pipe(res);
|
|
}
|
|
}
|
|
);
|
|
addErrorHandler(req, client);
|
|
reqBuff && client.end(reqBuff);
|
|
return client;
|
|
};
|
|
req.passThrough = function (uri, newTrailers) {
|
|
var handleReq;
|
|
var handleRes;
|
|
var transformReq;
|
|
var transformRes;
|
|
var reqBuf;
|
|
var resBuf;
|
|
if (typeof uri === 'function') {
|
|
handleReq = uri;
|
|
uri = null;
|
|
} else if (uri && typeof uri.transformReq === 'function') {
|
|
transformReq = uri.transformReq;
|
|
}
|
|
if (typeof newTrailers === 'function') {
|
|
handleRes = newTrailers;
|
|
newTrailers = null;
|
|
} else if (uri && typeof uri.transformRes === 'function') {
|
|
transformRes = uri.transformRes;
|
|
}
|
|
var _request = function(reqBody, _uri) {
|
|
var client = req.request(_uri || uri, function (_res) {
|
|
var _response = function(resBody, trailers) {
|
|
res.writeHead(_res.statusCode, _res.statusMessage, _res.headers);
|
|
appendTrailers(_res, res, trailers || newTrailers, req);
|
|
if (resBody === undefined) {
|
|
_res.pipe(res);
|
|
} else if (resBody && typeof resBody.pipe === 'function') {
|
|
resBody.pipe(res);
|
|
} else {
|
|
res.end(resBody || '');
|
|
}
|
|
};
|
|
|
|
var handleNextRes = function(result) {
|
|
if (result) {
|
|
req.setResRules(result.rules);
|
|
getCustomBody(isBodyData(result) ? result : result.body, _res, function(body) {
|
|
_response(body, result.trailers);
|
|
});
|
|
} else {
|
|
_response(result === undefined && (resBuf || _res._readRawBuffer_));
|
|
}
|
|
};
|
|
if (transformRes) {
|
|
transformRes(common.wrapReadStream(_res), handleNextRes);
|
|
} else if (handleRes) {
|
|
readStream(_res, function(rawBuffer) {
|
|
resBuf = rawBuffer;
|
|
handleRes(rawBuffer, handleNextRes, _res);
|
|
});
|
|
} else {
|
|
_response();
|
|
}
|
|
});
|
|
if (reqBody === undefined) {
|
|
req.pipe(client);
|
|
} else if (reqBody && typeof reqBody.pipe === 'function') {
|
|
reqBody.pipe(client);
|
|
} else {
|
|
client.end(reqBody || '');
|
|
}
|
|
};
|
|
var handleNextReq = function(result) {
|
|
if (result) {
|
|
req.setReqRules(result.rules);
|
|
getCustomBody(isBodyData(result) ? result : result.body, req, function(body) {
|
|
_request(body, result.uri || result.url);
|
|
});
|
|
} else {
|
|
_request(result === undefined && (reqBuf || req._readRawBuffer_));
|
|
}
|
|
};
|
|
if (transformReq) {
|
|
transformReq(common.wrapReadStream(req), handleNextReq);
|
|
} else if (handleReq) {
|
|
readStream(req, function(rawBuffer) {
|
|
reqBuf = rawBuffer;
|
|
handleReq(rawBuffer, handleNextReq, req);
|
|
});
|
|
} else {
|
|
_request();
|
|
}
|
|
};
|
|
httpServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.REQ_STATS:
|
|
if (statsServer) {
|
|
initReq(req, res);
|
|
statsServer.emit('request', req, res);
|
|
res.end();
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.RES_STATS:
|
|
if (resStatsServer) {
|
|
initReq(req, res);
|
|
resStatsServer.emit('request', req, res);
|
|
res.end();
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.REQ_RULES:
|
|
if (rulesServer) {
|
|
initReq(req, res);
|
|
rulesServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.RES_RULES:
|
|
if (resRulesServer) {
|
|
initReq(req, res);
|
|
resRulesServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_RULES:
|
|
if (tunnelRulesServer) {
|
|
initReq(req, res);
|
|
tunnelRulesServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
default:
|
|
res.destroy();
|
|
}
|
|
});
|
|
server.on('upgrade', function (req, socket, head) {
|
|
switch (getHookName(req)) {
|
|
case PLUGIN_HOOKS.UI:
|
|
if (uiServer) {
|
|
setContext(req);
|
|
uiServer.emit('upgrade', req, socket, head);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.HTTP:
|
|
if (httpServer) {
|
|
initReq(req, socket, true);
|
|
var curUrl = req.originalReq.realUrl;
|
|
httpServer.setMaxWsPayload = setMaxWsPayload;
|
|
var svrRes = '';
|
|
var reqRules, resRules;
|
|
req.setRules = req.setReqRules = socket.setReqRules = function (rules) {
|
|
reqRules = rules;
|
|
return true;
|
|
};
|
|
socket.setRules = req.setResRules = socket.setResRules = function (rules) {
|
|
resRules = rules;
|
|
return true;
|
|
};
|
|
req.writeHead = socket.writeHead = function (code, msg, headers) {
|
|
code = code > 0 ? code : svrRes.statusCode || 101;
|
|
if (msg && typeof msg !== 'string') {
|
|
headers = headers || msg;
|
|
msg = null;
|
|
}
|
|
headers = headers || svrRes.headers;
|
|
if (resRules) {
|
|
headers = headers || {};
|
|
setResRules(headers, resRules);
|
|
socket.setRules = req.setResRules = socket.setResRules = noop;
|
|
resRules = null;
|
|
}
|
|
headers = headers && formatRawHeaders(headers, svrRes);
|
|
msg = msg || svrRes.statusMessage || STATUS_CODES[code] || '';
|
|
socket.write(common.getHttpMeta('HTTP/1.1', code, msg, headers), 'binary');
|
|
};
|
|
req.request = function (uri, cb, opts) {
|
|
var args = normalizeArgs(uri, cb, req, curUrl, true, opts);
|
|
uri = args[0];
|
|
cb = args[1];
|
|
opts = args[3];
|
|
req.setRules = req.setReqRules = socket.setReqRules = noop;
|
|
setReqRules(uri, getReqRules(opts, reqRules));
|
|
reqRules = null;
|
|
var client = (args[2] ? httpsRequest : httpRequest)(uri);
|
|
var rawFrame;
|
|
if (opts === true) {
|
|
rawFrame = true;
|
|
} else if (opts) {
|
|
rawFrame = opts.rawFrame;
|
|
}
|
|
client.once('response', function(_res) {
|
|
svrRes = _res;
|
|
socket.writeHead();
|
|
_res.pipe(socket);
|
|
});
|
|
client.once('upgrade', function (_res, resSocket) {
|
|
svrRes = _res;
|
|
if (rawFrame !== true && common.isWebSocket(svrRes.headers)) {
|
|
var clientParser = addFrameHandler(
|
|
_res,
|
|
socket,
|
|
maxWsPayload,
|
|
true,
|
|
false
|
|
);
|
|
var serverParser = addFrameHandler(
|
|
_res,
|
|
resSocket,
|
|
maxWsPayload,
|
|
false,
|
|
true
|
|
);
|
|
handleWsSignal(
|
|
clientParser.receiver,
|
|
serverParser.sender
|
|
);
|
|
handleWsSignal(
|
|
serverParser.receiver,
|
|
clientParser.sender
|
|
);
|
|
}
|
|
if (typeof cb === 'function') {
|
|
resSocket.headers = _res.headers;
|
|
resSocket.statusCode = _res.statusCode;
|
|
resSocket.statusMessage = _res.statusMessage;
|
|
cb(resSocket);
|
|
} else {
|
|
socket.writeHead();
|
|
socket.pipe(resSocket).pipe(socket);
|
|
}
|
|
});
|
|
addErrorHandler(req, client);
|
|
client.end();
|
|
return client;
|
|
};
|
|
req.passThrough = function (uri) {
|
|
req.request(
|
|
uri,
|
|
function (resSock) {
|
|
socket.writeHead(
|
|
resSock.statusCode,
|
|
resSock.statusMessage,
|
|
resSock.headers
|
|
);
|
|
resSock.pipe(socket).pipe(resSock);
|
|
},
|
|
true
|
|
);
|
|
};
|
|
httpServer.emit('upgrade', req, socket, head);
|
|
}
|
|
break;
|
|
default:
|
|
socket.destroy();
|
|
}
|
|
});
|
|
|
|
var emitHttpPipe = function (httpServer, req, socket) {
|
|
if (httpServer) {
|
|
setupConnect(req, socket);
|
|
req.sendEstablished(function () {
|
|
var decoder = getDecodeTransform();
|
|
var encoder = getEncodeTransform();
|
|
var done;
|
|
var destroy = function () {
|
|
if (!done) {
|
|
done = true;
|
|
socket.destroy();
|
|
}
|
|
};
|
|
socket.on('error', function (err) {
|
|
if (!done) {
|
|
destroy();
|
|
decoder.emit('error', err);
|
|
encoder.emit('error', err);
|
|
}
|
|
});
|
|
socket.once('close', function () {
|
|
done = true;
|
|
decoder.emit('close');
|
|
encoder.emit('close');
|
|
});
|
|
decoder.destroy = encoder.destroy = destroy;
|
|
decoder.on('error', destroy);
|
|
encoder.on('error', destroy);
|
|
ADDITIONAL_FIELDS.forEach(function (key) {
|
|
decoder[key] = req[key];
|
|
});
|
|
encoder.once('finish', function() {
|
|
socket.end();
|
|
});
|
|
encoder.pipe(socket).pipe(decoder);
|
|
httpServer.emit('request', decoder, encoder);
|
|
});
|
|
}
|
|
};
|
|
|
|
function pipeUpgrade(pipeSvr, req, socket, head, fromClient, toServer) {
|
|
if (pipeSvr) {
|
|
setupConnect(req, socket);
|
|
req.sendEstablished(function () {
|
|
addFrameHandler(req, socket, maxWsPayload, fromClient, toServer);
|
|
pipeSvr.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
}
|
|
|
|
server.on('connect', function (req, socket, head) {
|
|
var pipeServer;
|
|
switch (getHookName(req)) {
|
|
case PLUGIN_HOOKS.REQ_READ:
|
|
emitHttpPipe(reqRead, req, socket);
|
|
break;
|
|
case PLUGIN_HOOKS.REQ_WRITE:
|
|
emitHttpPipe(reqWrite, req, socket);
|
|
break;
|
|
case PLUGIN_HOOKS.RES_READ:
|
|
emitHttpPipe(resRead, req, socket);
|
|
break;
|
|
case PLUGIN_HOOKS.RES_WRITE:
|
|
emitHttpPipe(resWrite, req, socket);
|
|
break;
|
|
case PLUGIN_HOOKS.WS_REQ_READ:
|
|
pipeUpgrade(wsReqRead, req, socket, head, true, true);
|
|
break;
|
|
case PLUGIN_HOOKS.WS_REQ_WRITE:
|
|
pipeUpgrade(wsReqWrite, req, socket, head, true, true);
|
|
break;
|
|
case PLUGIN_HOOKS.WS_RES_READ:
|
|
pipeUpgrade(wsResRead, req, socket, head);
|
|
break;
|
|
case PLUGIN_HOOKS.WS_RES_WRITE:
|
|
pipeUpgrade(wsResWrite, req, socket, head);
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL:
|
|
if (tunnelServer) {
|
|
setupConnect(req, socket, true);
|
|
req.sendEstablished(function () {
|
|
var reqRules;
|
|
req.setRules = req.setReqRules = socket.setReqRules = function (rules) {
|
|
reqRules = rules;
|
|
return true;
|
|
};
|
|
socket.setRules = req.setResRules = socket.setResRules = noop;
|
|
req.writeHead = socket.writeHead = noop;
|
|
req.connect = function (uri, cb, opts) {
|
|
var headers = req.headers;
|
|
var policy = headers['x-whistle-policy'];
|
|
if (common.isTunnelHost(uri)) {
|
|
headers.host = uri;
|
|
} else if (typeof uri === 'function') {
|
|
opts = cb;
|
|
cb = uri;
|
|
} else if (uri) {
|
|
if (common.isTunnelHost(uri.url)) {
|
|
if (uri.headers) {
|
|
headers = extend({}, uri.headers);
|
|
if (policy && !headers['x-whistle-policy']) {
|
|
headers['x-whistle-policy'] = policy;
|
|
}
|
|
}
|
|
headers.host = uri.url;
|
|
if (cb && typeof cb !== 'function') {
|
|
opts = cb;
|
|
cb = null;
|
|
}
|
|
}
|
|
opts = opts || uri;
|
|
}
|
|
uri = {
|
|
method: 'CONNECT',
|
|
agent: false,
|
|
path: headers.host,
|
|
headers: headers
|
|
};
|
|
req.setRules = req.setReqRules = socket.setReqRules = noop;
|
|
setReqRules(uri, getReqRules(opts, reqRules));
|
|
reqRules = null;
|
|
if (!opts || !notEmptyStr(opts.host)) {
|
|
headers[PROXY_ID_HEADER] = req[REQ_ID_KEY];
|
|
uri.host = boundIp;
|
|
uri.port = boundPort;
|
|
addRemoteInfo(req, headers);
|
|
} else {
|
|
uri.host = opts.host;
|
|
uri.port = opts.port > 0 ? opts.port : 80;
|
|
}
|
|
uri.headers = formatRawHeaders(headers, req);
|
|
var client = httpRequest(uri);
|
|
client.on('connect', function (_res, svrSock) {
|
|
if (_res.statusCode !== 200) {
|
|
var err = new Error(
|
|
'Tunneling socket could not be established, statusCode=' +
|
|
_res.statusCode
|
|
);
|
|
err.statusCode = _res.statusCode;
|
|
svrSock.destroy();
|
|
process.nextTick(function () {
|
|
client.emit('error', err);
|
|
});
|
|
return;
|
|
}
|
|
if (_res.headers[common.ALLOW_ACK]) {
|
|
svrSock.write('1');
|
|
}
|
|
if (typeof cb === 'function') {
|
|
svrSock.headers = _res.headers;
|
|
svrSock.statusCode = 200;
|
|
cb(svrSock);
|
|
} else {
|
|
svrSock.pipe(socket).pipe(svrSock);
|
|
}
|
|
});
|
|
addErrorHandler(req, client);
|
|
client.end();
|
|
return client;
|
|
};
|
|
req.passThrough = function (uri) {
|
|
req.connect(uri, function (svrSock) {
|
|
svrSock.pipe(socket).pipe(svrSock);
|
|
});
|
|
};
|
|
tunnelServer.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_REQ_READ:
|
|
setupConnect(req, socket);
|
|
pipeServer = req._isUpgrade ? wsReqRead : tunnelReqRead;
|
|
if (pipeServer) {
|
|
req.sendEstablished(function () {
|
|
socket.headers = req.headers;
|
|
if (req._isUpgrade ? wsReqWrite : tunnelReqWrite) {
|
|
wrapTunnelWriter(socket);
|
|
}
|
|
pipeServer.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_REQ_WRITE:
|
|
setupConnect(req, socket);
|
|
pipeServer = req._isUpgrade ? wsReqWrite : tunnelReqWrite;
|
|
if (pipeServer) {
|
|
req.sendEstablished(function () {
|
|
if (req._isUpgrade ? wsReqRead : tunnelReqRead) {
|
|
wrapTunnelReader(socket);
|
|
}
|
|
pipeServer.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_RES_READ:
|
|
setupConnect(req, socket);
|
|
pipeServer = req._isUpgrade ? wsResRead : tunnelResRead;
|
|
if (pipeServer) {
|
|
req.sendEstablished(function () {
|
|
socket.headers = req.headers;
|
|
if (req._isUpgrade ? wsResWrite : tunnelResWrite) {
|
|
wrapTunnelWriter(socket);
|
|
}
|
|
pipeServer.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_RES_WRITE:
|
|
setupConnect(req, socket);
|
|
pipeServer = req._isUpgrade ? wsResWrite : tunnelResWrite;
|
|
if (pipeServer) {
|
|
req.sendEstablished(function () {
|
|
if (req._isUpgrade ? wsResRead : tunnelResRead) {
|
|
wrapTunnelReader(socket);
|
|
}
|
|
pipeServer.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
default:
|
|
socket.destroy();
|
|
}
|
|
});
|
|
callbackHandler();
|
|
}
|
|
);
|
|
};
|
|
initProxy(async function(proxy) {
|
|
options.connect = proxy.connect;
|
|
options.request = proxy.request;
|
|
options.streamUtils = {
|
|
readRawBuffer: readStream,
|
|
readBuffer: function(stream, cb) {
|
|
readStream(stream, function() {
|
|
stream.getBuffer(function(err, buffer) {
|
|
if (err) {
|
|
return stream.emit('error', err);
|
|
}
|
|
cb(buffer);
|
|
});
|
|
});
|
|
},
|
|
readText: function(stream, cb) {
|
|
readStream(stream, function() {
|
|
stream.getText(function(err, buffer) {
|
|
if (err) {
|
|
return stream.emit('error', err);
|
|
}
|
|
cb(buffer);
|
|
});
|
|
});
|
|
},
|
|
readJson: function(stream, cb) {
|
|
readStream(stream, function() {
|
|
stream.getJson(function(err, data) {
|
|
if (err) {
|
|
return stream.emit('error', err);
|
|
}
|
|
cb(data);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
options.readStream = readStream;
|
|
options.requestInternal = function(options, cb) {
|
|
if (typeof options === 'string') {
|
|
options = { url: options };
|
|
} else if (options.headers) {
|
|
options = extend({}, options);
|
|
options.headers = extend({}, options.headers);
|
|
}
|
|
options.pluginName = null;
|
|
return request(setInternalOptions(options, {
|
|
PROXY_ID_HEADER: PROXY_ID_HEADER,
|
|
host: config.host,
|
|
port: config.port
|
|
}), cb);
|
|
};
|
|
compat.compatKeys(options);
|
|
compat.compatKeys(config);
|
|
var initial = loadModule(path.join(options.value, 'initial.js')) ||
|
|
loadModule(path.join(options.value, 'initialize.js'));
|
|
if (initial && typeof initial !== 'function') {
|
|
initial = initial.default;
|
|
}
|
|
if (typeof initial === 'function') {
|
|
if (initial.length === 2) {
|
|
ctx = await initial(options, initServers);
|
|
return ctx;
|
|
}
|
|
ctx = await initial(options);
|
|
}
|
|
initServers();
|
|
});
|
|
};
|