UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

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