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(); };