mirror of
https://github.com/avwo/whistle.git
synced 2026-10-22 01:06:41 +08:00
1675 lines
50 KiB
JavaScript
1675 lines
50 KiB
JavaScript
require('../util/patch');
|
|
var path = require('path');
|
|
var LRU = require('lru-cache');
|
|
var iconv = require('iconv-lite');
|
|
var wsParser = require('ws-parser');
|
|
var http = require('http');
|
|
var https = require('https');
|
|
var extend = require('extend');
|
|
var request = require('../util/http-mgr').request;
|
|
var isUtf8 = require('../util/is-utf8');
|
|
var Storage = require('../rules/storage');
|
|
var getServer = require('hagent').create(null, 40500);
|
|
var Buffer = require('safe-buffer').Buffer;
|
|
var parseUrl = require('../util/parse-url');
|
|
var hparser = require('hparser');
|
|
var transproto = require('../util/transproto');
|
|
var common = require('../util/common');
|
|
|
|
var getEncodeTransform = transproto.getEncodeTransform;
|
|
var getDecodeTransform = transproto.getDecodeTransform;
|
|
|
|
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 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 * 200;
|
|
var TIMEOUT = 300;
|
|
var REQ_INTERVAL = 16;
|
|
var pluginOpts, storage;
|
|
var pluginKeyMap = {};
|
|
var MASK_OPTIONS = { mask: true };
|
|
/* eslint-disable no-undef */
|
|
var REQ_ID_KEY = typeof Symbol === 'undefined' ? '$reqId_' + Date.now() : Symbol();
|
|
var SESSION_KEY = typeof Symbol === 'undefined' ? '$session_' + Date.now() : Symbol();
|
|
var FRAME_KEY = typeof Symbol === 'undefined' ? '$frame_' + Date.now() : Symbol();
|
|
var REQ_KEY = typeof Symbol === 'undefined' ? '$req_' + Date.now() : Symbol();
|
|
var CLOSED = typeof Symbol === 'undefined' ? '$colsed_' + Date.now() : Symbol();
|
|
/* eslint-enable no-undef */
|
|
var index = 1000;
|
|
var noop = function() {};
|
|
var ctx, PLUGIN_HOOK_NAME_HEADER;
|
|
|
|
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 getValue = function(req, name) {
|
|
var value = req.headers[name] || '';
|
|
try {
|
|
return value ? decodeURIComponent(value) : '';
|
|
} catch(e) {}
|
|
return String(value);
|
|
};
|
|
var setContext = function(req) {
|
|
if (ctx) {
|
|
ctx.request = req;
|
|
req.ctx = ctx;
|
|
}
|
|
req.localStorage = storage;
|
|
req.Storage = Storage;
|
|
req.clientIp = getValue(req, pluginOpts.CLIENT_IP_HEADER) || '127.0.0.1';
|
|
};
|
|
|
|
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 > 600) {
|
|
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 = 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 = toBuffer(frame.base64);
|
|
try {
|
|
buf && req.emit('sendToClient', buf, frame.binary);
|
|
} catch(e) {}
|
|
});
|
|
}
|
|
if (Array.isArray(data.toServer)) {
|
|
data.toServer.forEach(function(frame) {
|
|
var buf = toBuffer(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'
|
|
];
|
|
|
|
var initReq = function(req, res, isServer) {
|
|
var destroy = function(e) {
|
|
if (!req._hasError) {
|
|
req._hasError = true;
|
|
req.destroy && req.destroy(e);
|
|
res.destroy && res.destroy(e);
|
|
}
|
|
};
|
|
req.on('error', destroy);
|
|
res.on('error', destroy);
|
|
|
|
req.unsafe_getReqSession = function(cb) {
|
|
return getSession(req, cb, true);
|
|
};
|
|
req.unsafe_getSession = function(cb) {
|
|
return getSession(req, cb);
|
|
};
|
|
req.unsafe_getFrames = function(cb) {
|
|
return getFrames(req, cb);
|
|
};
|
|
req.getReqSession = function(cb) {
|
|
return getSession(req, cb, true);
|
|
};
|
|
req.getSession = function(cb) {
|
|
return getSession(req, cb);
|
|
};
|
|
req.getFrames = function(cb) {
|
|
return getFrames(req, cb);
|
|
};
|
|
|
|
var reqId = getValue(req, pluginOpts.REQ_ID_HEADER);
|
|
var oReq = req.originalReq = req.request = {};
|
|
var oRes = req.originalRes = req.response = {};
|
|
setContext(req);
|
|
oReq.clientIp = req.clientIp;
|
|
if (isServer) {
|
|
var customParserHeader = req.headers[pluginOpts.CUSTOM_PARSER_HEADER];
|
|
if (customParserHeader && typeof customParserHeader === 'string') {
|
|
addParserApi(req, res, customParserHeader, reqId);
|
|
req.customParser = oReq.customParser = true;
|
|
}
|
|
}
|
|
var headers = extractHeaders(req, pluginKeyMap);
|
|
req[REQ_ID_KEY] = oReq.id = reqId;
|
|
addSessionStorage(req, reqId);
|
|
oReq.headers = headers;
|
|
oReq.isFromPlugin = headers[pluginOpts.PLUGIN_REQUEST_HEADER] == '1';
|
|
oReq.ruleValue = getValue(req, pluginOpts.RULE_VALUE_HEADER);
|
|
oReq.ruleUrl = getValue(req, pluginOpts.RULE_URL_HEADER) || oReq.ruleValue;
|
|
oReq.pipeValue = getValue(req, pluginOpts.PIPE_VALUE_HEADER);
|
|
oReq.hostValue = getValue(req, pluginOpts.HOST_VALUE_HEADER);
|
|
oReq.url = oReq.fullUrl = getValue(req, pluginOpts.FULL_URL_HEADER);
|
|
oReq.realUrl = getValue(req, pluginOpts.REAL_URL_HEADER) || oReq.url;
|
|
oReq.relativeUrl = getValue(req, pluginOpts.RELATIVE_URL_HEADER);
|
|
oReq.method = getValue(req, pluginOpts.METHOD_HEADER) || 'GET';
|
|
oReq.clientPort = getValue(req, pluginOpts.CLIENT_PORT_HEAD);
|
|
oReq.globalValue = getValue(req, pluginOpts.GLOBAL_VALUE_HEAD);
|
|
oReq.proxyValue = getValue(req, pluginOpts.PROXY_VALUE_HEADER);
|
|
oReq.pacValue = getValue(req, pluginOpts.PAC_VALUE_HEADER);
|
|
oRes.serverIp = getValue(req, pluginOpts.HOST_IP_HEADER) || '127.0.0.1';
|
|
oRes.statusCode = getValue(req, pluginOpts.STATUS_CODE_HEADER);
|
|
};
|
|
var toBuffer = function(base64) {
|
|
if (base64) {
|
|
try {
|
|
return new Buffer(base64, 'base64');
|
|
} catch(e) {}
|
|
}
|
|
};
|
|
var getBuffer = function(item) {
|
|
return toBuffer(item.base64);
|
|
};
|
|
var getText = function(item) {
|
|
var body = toBuffer(item.base64) || '';
|
|
if (body && !isUtf8(body)) {
|
|
try {
|
|
body = iconv.encode(body, 'GB18030');
|
|
} catch(e) {}
|
|
}
|
|
return 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);
|
|
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];
|
|
framesPending = true;
|
|
var query = '?curReqId=' + req[REQ_ID_KEY] + '&lastFrameId=' + (req[FRAME_KEY] || '');
|
|
framesOpts.path = framesOpts.path.replace(QUERY_RE, query);
|
|
requestData(framesOpts, function(err, result) {
|
|
framesPending = false;
|
|
if (err || !result) {
|
|
framesCallbacks.push(cb);
|
|
return retryRequestFrames();
|
|
}
|
|
var frames = result.frames;
|
|
var closed = !frames;
|
|
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);
|
|
}
|
|
}
|
|
if (!frames || frames.length) {
|
|
try {
|
|
cb(frames || '');
|
|
} catch(e) {}
|
|
} else {
|
|
framesCallbacks.push(cb);
|
|
}
|
|
req[CLOSED] = closed;
|
|
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);
|
|
});
|
|
};
|
|
|
|
var getFrames = function(req, cb) {
|
|
var reqId = req[REQ_ID_KEY];
|
|
if (!REQ_ID_RE.test(reqId) || typeof cb !== 'function') {
|
|
return;
|
|
}
|
|
var url = req.originalReq.url;
|
|
var isTunnel = !req[CLOSED] && /^tunnel/.test(url);
|
|
if (!isTunnel && !/^ws/.test(url)) {
|
|
return cb('');
|
|
}
|
|
cb[REQ_KEY] = req;
|
|
framesCallbacks.push(cb);
|
|
getSession(req, function(session) {
|
|
if (!session || session.reqError || session.resError
|
|
|| (isTunnel && !session.inspect)) {
|
|
framesCallbacks.forEach(function(_cb) {
|
|
req[CLOSED] = 1;
|
|
_cb('');
|
|
});
|
|
framesCallbacks = [];
|
|
return;
|
|
}
|
|
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) {
|
|
var established;
|
|
initWsReq(req, res);
|
|
req.sendEstablished = function(err, cb) {
|
|
if (established) {
|
|
return;
|
|
}
|
|
if (typeof err === 'function') {
|
|
cb = err;
|
|
err = null;
|
|
}
|
|
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['x-whistle-request-tunnel-ack']) {
|
|
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();
|
|
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);
|
|
}
|
|
|
|
function toBinary(data) {
|
|
if (!data || Buffer.isBuffer(data)) {
|
|
return data && data.length && data;
|
|
}
|
|
if (typeof data !== 'string') {
|
|
try {
|
|
data = JSON.stringify(data);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
}
|
|
return Buffer.from(data);
|
|
}
|
|
|
|
function wrapTunnelWriter(socket) {
|
|
var write = socket.write;
|
|
var end = socket.end;
|
|
var sender = wsParser.getSender(socket);
|
|
handleError(socket, sender);
|
|
socket.write = function(chunk, encoding, cb) {
|
|
if (chunk = toBinary(chunk)) {
|
|
if (encoding === 'binary') {
|
|
return write.call(this, chunk, encoding, cb);
|
|
}
|
|
sender.send(chunk);
|
|
}
|
|
};
|
|
socket.end = function(chunk) {
|
|
socket.write(chunk);
|
|
return end.call(this);
|
|
};
|
|
}
|
|
|
|
function wrapTunnelReader(socket) {
|
|
socket.wsExts = '';
|
|
var receiver = wsParser.getReceiver(socket);
|
|
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) {
|
|
emit.call(socket, 'data', chunk);
|
|
};
|
|
}
|
|
|
|
function addFrameHandler(req, socket, maxWsPayload, fromClient, toServer) {
|
|
socket.wsExts = req.headers['sec-websocket-extensions'] || '';
|
|
var receiver = wsParser.getReceiver(socket, !fromClient, maxWsPayload);
|
|
var emit = socket.emit;
|
|
var write = socket.write;
|
|
var end = socket.end;
|
|
var sender = wsParser.getSender(socket, toServer);
|
|
var getOptions = function(opts) {
|
|
if (typeof opts !== 'object') {
|
|
opts = null;
|
|
} else if (opts) {
|
|
opts.mask = toServer;
|
|
opts.binary = opts.binary || opts.opcode == 2;
|
|
}
|
|
return opts || (toServer ? MASK_OPTIONS : '');
|
|
};
|
|
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) {
|
|
emit.call(socket, 'data', chunk, opts);
|
|
};
|
|
socket.write = function(chunk, opts, cb) {
|
|
if (chunk = toBinary(chunk)) {
|
|
if (opts === 'binary') {
|
|
return write.call(this, chunk, opts, cb);
|
|
}
|
|
sender.send(chunk, getOptions(opts));
|
|
}
|
|
};
|
|
socket.end = function(chunk) {
|
|
socket.write(chunk);
|
|
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 extractHeaders(req, exlucdekyes) {
|
|
var headers = {};
|
|
Object.keys(req.headers).forEach(function(key) {
|
|
if (!exlucdekyes[key]) {
|
|
headers[key] = req.headers[key];
|
|
}
|
|
});
|
|
return headers;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
module.exports = function(options, callback) {
|
|
options.Storage = Storage;
|
|
options.parseUrl = parseUrl;
|
|
options.formatHeaders = formatRawHeaders;
|
|
var config = options.config;
|
|
var boundIp = config.host;
|
|
var PROXY_ID_HEADER = config.PROXY_ID_HEADER;
|
|
var name = options.name;
|
|
var PLUGIN_HOOKS = options.config.PLUGIN_HOOKS;
|
|
var RES_RULES_HEAD = options.config.RES_RULES_HEAD;
|
|
var getRulesStr = function(obj) {
|
|
obj = JSON.stringify({
|
|
rules: typeof obj === 'string' ? obj : obj.rules,
|
|
values: obj.values,
|
|
root: options.value
|
|
});
|
|
return encodeURIComponent(obj);
|
|
};
|
|
|
|
PLUGIN_HOOK_NAME_HEADER = options.config.PLUGIN_HOOK_NAME_HEADER;
|
|
options.shortName = name.substring(name.indexOf('/') + 1);
|
|
storage = new Storage(path.join(options.config.baseDir, '.plugins', options.name));
|
|
options.storage = options.localStorage = storage;
|
|
pluginOpts = options;
|
|
Object.keys(options).forEach(function(key) {
|
|
key = options[key];
|
|
if (typeof key === 'string' && !key.indexOf('x-whistle-')) {
|
|
pluginKeyMap[key] = 1;
|
|
}
|
|
});
|
|
var authKey = config.authKey;
|
|
delete config.authKey;
|
|
var headers = {
|
|
'x-whistle-auth-key': authKey,
|
|
'content-type': 'application/json'
|
|
};
|
|
var baseUrl = 'http://' + boundIp + ':' + config.port + '/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;
|
|
if (type !== 'string') {
|
|
if (uri && type === 'object') {
|
|
headers = uri.headers;
|
|
method = typeof uri.method === 'string' ? uri.method : 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 || extractHeaders(req, pluginKeyMap);
|
|
if (isWs) {
|
|
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 = config.port;
|
|
if (isHttps) {
|
|
headers[config.HTTPS_FIELD] = 1;
|
|
if (alpnProtocol) {
|
|
headers[config.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[config.HTTPS_FIELD] = 1;
|
|
} else {
|
|
uri.protocol = 'https:';
|
|
}
|
|
}
|
|
}
|
|
delete uri.hostname;
|
|
uri.headers = formatRawHeaders(headers, req);
|
|
uri.agent = false;
|
|
return [uri, cb, isHttps, opts];
|
|
};
|
|
|
|
var port, statsPort, resStatsPort, uiPort, rulesPort, resRulesPort, tunnelRulesPort, tunnelPort;
|
|
var reqWritePort, reqReadPort, resWritePort, resReadPort;
|
|
var wsReqWritePort, wsReqReadPort, wsResWritePort, wsResReadPort;
|
|
var tunnelReqWritePort, tunnelReqReadPort, tunnelResWritePort, tunnelResReadPort;
|
|
var callbackHandler = function() {
|
|
callback(null, {
|
|
port: port,
|
|
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 rulesUrlOpts = parseCgiUrl(baseUrl + 'rules/list2');
|
|
var valuesUrlOpts = parseCgiUrl(baseUrl + 'values/list2');
|
|
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 updateTimer;
|
|
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) || '');
|
|
} catch (e) {}
|
|
}
|
|
cb('', res);
|
|
});
|
|
};
|
|
|
|
var updateRules = function() {
|
|
requestCgi(updateRulesOpts, noop);
|
|
updateTimer = null;
|
|
};
|
|
options.updateRules = function() {
|
|
updateTimer = updateTimer || setTimeout(updateRules, 500);
|
|
};
|
|
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'));
|
|
});
|
|
};
|
|
options.getRules = function(cb) {
|
|
requestCgi(rulesUrlOpts, cb);
|
|
};
|
|
options.getValues = function(cb) {
|
|
requestCgi(valuesUrlOpts, 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 initServers = function(_ctx) {
|
|
ctx = _ctx || ctx;
|
|
var execPlugin = require(options.value) || '';
|
|
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 hasServer = 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(function(server, _port) {
|
|
var maxWsPayload;
|
|
var 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;
|
|
};
|
|
|
|
if (startUIServer) {
|
|
uiServer = createServer();
|
|
startUIServer(uiServer, options);
|
|
uiPort = _port;
|
|
}
|
|
|
|
if (startServer) {
|
|
httpServer = createServer();
|
|
startServer(httpServer, options);
|
|
port = _port;
|
|
}
|
|
|
|
if (startStatsServer) {
|
|
statsServer = createServer();
|
|
startStatsServer(statsServer, options);
|
|
statsPort = _port;
|
|
}
|
|
|
|
if (startResStatsServer) {
|
|
resStatsServer = createServer();
|
|
startResStatsServer(resStatsServer, options);
|
|
resStatsPort = _port;
|
|
}
|
|
|
|
if (startRulesServer) {
|
|
rulesServer = createServer();
|
|
startRulesServer(rulesServer, options);
|
|
rulesPort = _port;
|
|
}
|
|
|
|
if (startResRulesServer) {
|
|
resRulesServer = createServer();
|
|
startResRulesServer(resRulesServer, options);
|
|
resRulesPort = _port;
|
|
}
|
|
|
|
if (startTunnelRulesServer) {
|
|
tunnelRulesServer = createServer();
|
|
startTunnelRulesServer(tunnelRulesServer, options);
|
|
tunnelRulesPort = _port;
|
|
}
|
|
|
|
if (startTunnelServer) {
|
|
tunnelServer = createServer();
|
|
startTunnelServer(tunnelServer, options);
|
|
tunnelPort = _port;
|
|
}
|
|
|
|
if (startReqRead) {
|
|
reqRead = createServer();
|
|
startReqRead(reqRead, options);
|
|
reqReadPort = _port;
|
|
}
|
|
|
|
if (startReqWrite) {
|
|
reqWrite = createServer();
|
|
startReqWrite(reqWrite, options);
|
|
reqWritePort = _port;
|
|
}
|
|
|
|
if (startResRead) {
|
|
resRead = createServer();
|
|
startResRead(resRead, options);
|
|
resReadPort = _port;
|
|
}
|
|
|
|
if (startResWrite) {
|
|
resWrite = createServer();
|
|
startResWrite(resWrite, options);
|
|
resWritePort = _port;
|
|
}
|
|
|
|
if (startWsReqRead) {
|
|
wsReqRead = createServer();
|
|
wsReqRead.setMaxWsPayload = setMaxWsPayload;
|
|
startWsReqRead(wsReqRead, options);
|
|
wsReqReadPort = _port;
|
|
}
|
|
|
|
if (startWsReqWrite) {
|
|
wsReqWrite = createServer();
|
|
wsReqWrite.setMaxWsPayload = setMaxWsPayload;
|
|
startWsReqWrite(wsReqWrite, options);
|
|
wsReqWritePort = _port;
|
|
}
|
|
|
|
if (startWsResRead) {
|
|
wsResRead = createServer();
|
|
wsResRead.setMaxWsPayload = setMaxWsPayload;
|
|
startWsResRead(wsResRead, options);
|
|
wsResReadPort = _port;
|
|
}
|
|
|
|
if (startWsResWrite) {
|
|
wsResWrite = createServer();
|
|
wsResWrite.setMaxWsPayload = setMaxWsPayload;
|
|
startWsResWrite(wsResWrite, options);
|
|
wsResWritePort = _port;
|
|
}
|
|
|
|
if (startTunnelReqRead) {
|
|
tunnelReqRead = createServer();
|
|
startTunnelReqRead(tunnelReqRead, options);
|
|
tunnelReqReadPort = _port;
|
|
}
|
|
|
|
if (startTunnelReqWrite) {
|
|
tunnelReqWrite = createServer();
|
|
startTunnelReqWrite(tunnelReqWrite, options);
|
|
tunnelReqWritePort = _port;
|
|
}
|
|
|
|
if (startTunnelResRead) {
|
|
tunnelResRead = createServer();
|
|
startTunnelResRead(tunnelResRead, options);
|
|
tunnelResReadPort = _port;
|
|
}
|
|
|
|
if (startTunnelResWrite) {
|
|
tunnelResWrite = createServer();
|
|
startTunnelResWrite(tunnelResWrite, options);
|
|
tunnelResWritePort = _port;
|
|
}
|
|
server.timeout = config.timeout;
|
|
server.on('request', function(req, res) {
|
|
switch(getHookName(req)) {
|
|
case PLUGIN_HOOKS.UI:
|
|
if (uiServer) {
|
|
setContext(req);
|
|
uiServer.emit('request', req, res);
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.HTTP:
|
|
if (httpServer) {
|
|
initReq(req, res);
|
|
var alpnProtocol = req.headers[config.ALPN_PROTOCOL_HEADER];
|
|
var curUrl = req.originalReq.realUrl;
|
|
var svrRes = '';
|
|
var resRules;
|
|
var writeHead = res.writeHead;
|
|
req.setResRules = res.setResRules = function(rules) {
|
|
resRules = rules;
|
|
};
|
|
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 || {};
|
|
headers[RES_RULES_HEAD] = getRulesStr(resRules);
|
|
}
|
|
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];
|
|
uri.agent = false;
|
|
var client = (args[2] ? httpsRequest : httpRequest)(uri, function(_res) {
|
|
svrRes = _res;
|
|
res.once('_wroteHead', function() {
|
|
appendTrailers(_res, res, args[3] && args[3].trailers, req);
|
|
});
|
|
if (typeof cb === 'function') {
|
|
cb.call(this, _res);
|
|
} else {
|
|
res.writeHead();
|
|
_res.pipe(res);
|
|
}
|
|
});
|
|
addErrorHandler(req, client);
|
|
return client;
|
|
};
|
|
req.passThrough = function(uri, newTrailers) {
|
|
var client = req.request(uri, function(_res) {
|
|
res.writeHead(_res.statusCode, _res.statusMessage, _res.headers);
|
|
appendTrailers(_res, res, newTrailers, req);
|
|
_res.pipe(res);
|
|
});
|
|
req.pipe(client);
|
|
};
|
|
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) {
|
|
initWsReq(req, socket);
|
|
var curUrl = req.originalReq.realUrl;
|
|
httpServer.setMaxWsPayload = setMaxWsPayload;
|
|
var svrRes = '';
|
|
var resRules;
|
|
req.setResRules = socket.setResRules = function(rules) {
|
|
resRules = rules;
|
|
};
|
|
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 || {};
|
|
headers[RES_RULES_HEAD] = getRulesStr(resRules);
|
|
}
|
|
headers = [
|
|
'HTTP/1.1 ' + code + ' ' + (msg || svrRes.statusMessage || STATUS_CODES[code] || ''),
|
|
headers ? getRawHeaders(formatRawHeaders(headers, svrRes)) : '',
|
|
'\r\n'
|
|
];
|
|
socket.write(headers.join('\r\n'), '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];
|
|
uri.agent = false;
|
|
var client = (args[2] ? httpsRequest : httpRequest)(uri);
|
|
var rawFrame;
|
|
if (opts === true) {
|
|
rawFrame = true;
|
|
} else if (opts) {
|
|
rawFrame = opts.rawFrame;
|
|
}
|
|
client.on('upgrade', function(_res, resSocket) {
|
|
svrRes = _res;
|
|
if (rawFrame !== true) {
|
|
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);
|
|
return client.end();
|
|
};
|
|
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) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
var decoder = getDecodeTransform();
|
|
var encoder = getEncodeTransform();
|
|
var done;
|
|
var handleError = function(err) {
|
|
if (!done) {
|
|
done = true;
|
|
socket.emit('error', err || new Error('destroyed'));
|
|
}
|
|
};
|
|
socket.on('error', function(err) {
|
|
if (!done) {
|
|
done = true;
|
|
decoder.emit('error', err);
|
|
encoder.emit('error', err);
|
|
}
|
|
});
|
|
encoder.once('finish', function() {
|
|
done = true;
|
|
});
|
|
socket.on('close', function() {
|
|
done = true;
|
|
decoder.emit('close');
|
|
encoder.emit('close');
|
|
});
|
|
decoder.destroy = encoder.destroy = handleError;
|
|
decoder.on('error', handleError);
|
|
encoder.on('error', handleError);
|
|
ADDITIONAL_FIELDS.forEach(function(key) {
|
|
decoder[key] = req[key];
|
|
});
|
|
socket.pipe(decoder);
|
|
encoder.pipe(socket);
|
|
socket.on('end', destroySocket);
|
|
httpServer.emit('request', decoder, encoder);
|
|
});
|
|
}
|
|
};
|
|
|
|
server.on('connect', function(req, socket, head) {
|
|
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:
|
|
if (wsReqRead) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
addFrameHandler(req, socket, maxWsPayload, true, true);
|
|
wsReqRead.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.WS_REQ_WRITE:
|
|
if (wsReqWrite) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
addFrameHandler(req, socket, maxWsPayload, true, true);
|
|
wsReqWrite.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.WS_RES_READ:
|
|
if (wsResRead) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
addFrameHandler(req, socket, maxWsPayload);
|
|
wsResRead.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.WS_RES_WRITE:
|
|
if (wsResWrite) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
addFrameHandler(req, socket, maxWsPayload);
|
|
wsResWrite.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL:
|
|
if (tunnelServer) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
req.connect = function(cb, opts) {
|
|
var headers = extractHeaders(req, pluginKeyMap);
|
|
var policy = headers['x-whistle-policy'];
|
|
if (policy) {
|
|
headers['x-whistle-policy'] = policy;
|
|
}
|
|
var uri = {
|
|
method: 'CONNECT',
|
|
agent: false,
|
|
path: headers.host
|
|
};
|
|
if (cb && typeof cb !== 'function') {
|
|
opts = cb;
|
|
cb = null;
|
|
}
|
|
if (!opts || !notEmptyStr(opts.host)) {
|
|
headers[PROXY_ID_HEADER] = req[REQ_ID_KEY];
|
|
uri.host = boundIp;
|
|
uri.port = config.port;
|
|
} 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['x-whistle-allow-tunnel-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);
|
|
return client.end();
|
|
};
|
|
req.passThrough = function() {
|
|
req.connect(function(svrSock) {
|
|
svrSock.pipe(socket).pipe(svrSock);
|
|
});
|
|
};
|
|
tunnelServer.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_REQ_READ:
|
|
if (tunnelReqRead) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
socket.headers = req.headers;
|
|
tunnelReqWrite && wrapTunnelWriter(socket);
|
|
tunnelReqRead.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_REQ_WRITE:
|
|
if (tunnelReqWrite) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
tunnelReqRead && wrapTunnelReader(socket);
|
|
tunnelReqWrite.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_RES_READ:
|
|
if (tunnelResRead) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
socket.headers = req.headers;
|
|
tunnelResWrite && wrapTunnelWriter(socket);
|
|
tunnelResRead.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
case PLUGIN_HOOKS.TUNNEL_RES_WRITE:
|
|
if (tunnelResWrite) {
|
|
initConnectReq(req, socket);
|
|
req.sendEstablished(function() {
|
|
tunnelResRead && wrapTunnelReader(socket);
|
|
tunnelResWrite.emit('connect', req, socket, head);
|
|
});
|
|
}
|
|
break;
|
|
default:
|
|
socket.destroy();
|
|
}
|
|
});
|
|
callbackHandler();
|
|
});
|
|
};
|
|
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 = initial(options, initServers);
|
|
return ctx;
|
|
}
|
|
ctx = initial(options);
|
|
}
|
|
initServers();
|
|
};
|
|
|
|
|