UNPKG

whistle

Version:

HTTP, HTTPS, Websocket debugging proxy

388 lines (350 loc) 12.1 kB
var path = require('path'); var os = require('os'); var http = require('http'); var crypto = require('crypto'); var httpAgent = http.Agent; var httpsAgent = require('https').Agent; var url = require('url'); var fse = require('fs-extra'); var _extend = require('util')._extend; var pkgConf = require('../package.json'); var config = _extend(exports, pkgConf); var tunnel = require('./util/agent'); var socks = require('socksv5'); var httpsAgents = {}; var httpAgents = {}; var socksAgents = {}; var uid = Date.now() + '-' + process.pid; var noop = function() {}; var LOCAL_UI_HOST_LIST = ['local.whistlejs.com', 'local.wproxy.org', 'rootca.pro']; var PLUGIN_RE = /^([a-z\d_\-]+)\.(.+)$/; var INTER_PORT_RE = /^(?:(\d{1,5})\.)?([a-z\d_\-]+)\.(.+)$/; var idleTimeout = 60000; var variableProperties = ['port', 'ATS', 'sockets', 'timeout', 'dataDirname', 'storage', 'baseDir', 'username', 'password', 'uipath', 'debug', 'debugMode', 'localUIHost', 'extra', 'rules', 'values']; config.ASSESTS_PATH = path.join(__dirname, '../assets'); config.WHISTLE_POLICY_HEADER = 'x-whistle-policy'; config.HTTPS_FIELD = 'x-' + config.name + '-https-request'; config.DATA_ID = 'x-' + config.name + '-data-id' + '-' + uid; config.CLIENT_IP_HEAD = 'x-forwarded-for-' + config.name + '-' + uid; config.HTTPS_FLAG = config.whistleSsl + '.'; config.WEBUI_HEAD = 'x-forwarded-from-' + config.name + '-' + uid; function getHomedir() { //默认设置为`~`,防止Linux在开机启动时Node无法获取homedir return (typeof os.homedir == 'function' ? os.homedir() : process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME']) || '~'; } function getWhistlePath() { return process.env.WHISTLE_PATH || path.join(getHomedir(), '.WhistleAppData'); } function getDataDir(dirname) { var dir = path.join(getWhistlePath(), dirname || '.' + config.name); fse.ensureDirSync(dir); return dir; } exports.getDataDir = getDataDir; try { var async_id_symbol = process.binding('async_wrap').async_id_symbol; } catch (e) {} function createAgent(agentConfig, https) { var agent = new (https ? httpsAgent : httpAgent)(agentConfig); if (async_id_symbol) { var addRequest = agent.addRequest; agent.addRequest = function(req) { var onSocket = req.onSocket; req.onSocket = function(socket) { try { socket[async_id_symbol] = socket._handle.getAsyncId(); } catch(e) {} onSocket.apply(this, arguments); }; addRequest.apply(this, arguments); }; } var createConnection = agent.createConnection; agent.createConnection = function() { var s = createConnection.apply(this, arguments); s.setTimeout(idleTimeout, function() { s.destroy(); }); return s; }; agent.on('free', preventThrowOutError); return agent; } function getHttpsAgent(options) { return getAgent(options, httpsAgents, 'httpsOverHttp'); } exports.getHttpsAgent = getHttpsAgent; function getHttpAgent(options) { return getAgent(options, httpAgents, 'httpOverHttp'); } exports.getHttpAgent = getHttpAgent; function getAgent(options, cache, type) { var key = getCacheKey(options); var agent = cache[key]; if (!agent) { options.proxyAuth = options.auth; options = { proxy: options, rejectUnauthorized: false }; agent = cache[key] = new tunnel[type || 'httpsOverHttp'](options); agent.on('free', preventThrowOutError); var createSocket = agent.createSocket; agent.createSocket = function(options, cb) { createSocket.call(this, options, function(socket) { socket.setTimeout(idleTimeout, function() { socket.destroy(); }); cb(socket); }); }; } return agent; } function getCacheKey(options) { return [options.isHttps ? 'https' : 'http', options.host, options.port, options.auth || options.proxyAuth || ''].join(':'); } function getAuths(_url) { var options = typeof _url == 'string' ? url.parse(_url) : _url; if (!options || !options.auth) { return [socks.auth.None()]; } var auths = []; options.auth.split('|').forEach(function(auth) { auth = auth.trim(); if (auth) { var index = auth.indexOf(':'); auths.push({ username: index == -1 ? auth : auth.substring(0, index), password: index == -1 ? '' : auth.substring(index + 1) }); } }); return auths.length ? auths.map(function(auth) { return socks.auth.UserPassword(auth.username, auth.password); }) : [socks.auth.None()]; } exports.getAuths = getAuths; exports.setAuth = function(auth) { if (!auth) { return; } config.username = auth.username; config.password = auth.password; }; function toBuffer(buf) { if (buf == null || buf instanceof Buffer) { return buf; } buf += ''; return new Buffer(buf); } exports.toBuffer = toBuffer; function connect(options, cb) { var proxyOptions = { method: 'CONNECT', agent: false, path: options.host + ':' + options.port, host: options.proxyHost, port: options.proxyPort, headers: options.headers || {} }; proxyOptions.headers.host = proxyOptions.path; if (options.proxyAuth) { proxyOptions.headers['proxy-authorization'] = 'Basic ' + toBuffer(options.proxyAuth).toString('base64'); } var timer = setTimeout(function() { req.emit('error', new Error('Timeout')); req.abort(); }, 16000); var req = http.request(proxyOptions); req.on('connect', function(res, socket, head) { clearTimeout(timer); socket.on('error', noop); cb(socket); if (res.statusCode !== 200) { process.nextTick(function() { req.emit('error', new Error('Tunneling socket could not be established, statusCode=' + res.statusCode)); }); } }).end(); return req; } exports.connect = connect; function preventThrowOutError(socket) { socket.removeListener('error', freeSocketErrorListener); socket.on('error', freeSocketErrorListener); } function freeSocketErrorListener() { var socket = this; socket.destroy(); socket.emit('agentRemove'); socket.removeListener('error', freeSocketErrorListener); } function resolvePath(file) { if (!file || !(file = file.trim())) { return file; } return /^[\w-]+$/.test(file) ? file : path.resolve(file); } function getHostname(_url) { if (typeof _url != 'string') { return ''; } if (_url.indexOf('/') != -1) { return url.parse(_url).hostname; } var index = _url.indexOf(':'); return index == -1 ? _url : _url.substring(0, index); } function getPluginPaths(newConf) { var pluginPaths = newConf.pluginPaths; if (!Array.isArray(pluginPaths)) { pluginPaths = [pluginPaths]; } pluginPaths = pluginPaths.filter(function(path) { return path && typeof path === 'string'; }); return pluginPaths.length ? pluginPaths : null; } exports.extend = function extend(newConf) { if (newConf) { variableProperties.forEach(function(name) { config[name] = newConf[name] || pkgConf[name]; }); config.disableAllRules = newConf.disableAllRules; config.disableAllPlugins = newConf.disableAllPlugins; config.allowMultipleChoice = newConf.allowMultipleChoice; if (newConf.replaceExistRule === false) { config.replaceExistRule = false; } else { config.replaceExistValue = newConf.replaceRules; } if (newConf.replaceExistValue === false) { config.replaceExistValue = false; } else { config.replaceExistValue = newConf.replaceValues; } if (newConf.certDir && typeof newConf.certDir === 'string') { config.certDir = newConf.certDir; } if (Array.isArray(newConf.ports)) { config.ports = pkgConf.ports.concat(newConf.ports); } if (typeof newConf.middlewares == 'string') { config.middlewares = newConf.middlewares.trim().split(/\s*,\s*/g); } config.pluginPaths = getPluginPaths(newConf); } if (config.timeout > idleTimeout) { idleTimeout = +config.timeout; } config.middlewares = Array.isArray(config.middlewares) ? config.middlewares.map(resolvePath) : []; config.localUIHost = getHostname(config.localUIHost); if (config.localUIHost && LOCAL_UI_HOST_LIST.indexOf(config.localUIHost) == -1) { LOCAL_UI_HOST_LIST.push(config.localUIHost); } config.localUIHost = 'local.whistlejs.com'; config.WEINRE_HOST = 'weinre.' + config.localUIHost; var isLocalUIUrl = function(url) { return LOCAL_UI_HOST_LIST.indexOf(getHostname(url)) != -1; }; config.isLocalUIUrl = isLocalUIUrl; var parseInternalUrl = function(url) { var host = getHostname(url); if (INTER_PORT_RE.test(host)) { var port = RegExp.$1; var name = RegExp.$2; if (isLocalUIUrl(RegExp.$3)) { return { port: port, name: name }; } } }; exports.parseInternalUrl = parseInternalUrl; config.isPluginUrl = function(url) { var host = getHostname(url); return PLUGIN_RE.test(host) && isLocalUIUrl(RegExp.$2); }; config.getPluginName = function(url) { var host = getHostname(url); if (PLUGIN_RE.test(host)) { var name = RegExp.$1; if (isLocalUIUrl(RegExp.$2)) { return name; } } }; var port = config.port; config.ports.forEach(function(name) { if (!/port$/.test(name) || name == 'port') { throw new Error('Port name "' + name + '" must be end of "port", but not equals "port", like: ' + name + 'port'); } config[name] = ++port; }); config.sockets = Math.max(parseInt(config.sockets, 10) || 0, 1); var agentConfig = { maxSockets: config.sockets, keepAlive: config.keepAlive, keepAliveMsecs: config.keepAliveMsecs }; config.httpAgent = config.debug ? false : createAgent(agentConfig); config.httpsAgent = config.debug ? false : createAgent(agentConfig, true); config.getSocksAgent = function(options) { var key = getCacheKey(options); var agent = socksAgents[key]; if (!agent) { var proxyOptions = _extend({}, agentConfig); proxyOptions.proxyHost = options.host; proxyOptions.proxyPort = parseInt(options.port, 10) || 1080; proxyOptions.rejectUnauthorized = false; proxyOptions.localDNS = false; proxyOptions.auths = getAuths(options); agent = socksAgents[key] = options.isHttps ? new socks.HttpsAgent(proxyOptions) : new socks.HttpAgent(proxyOptions); agent.on('free', preventThrowOutError); var createSocket = agent.createSocket; agent.createSocket = function(req, options) { var client = createSocket.apply(this, arguments); client.on('error', function(err) { req.emit('error', err); }); return client; }; } return agent; }; config.uipath = config.uipath ? resolvePath(config.uipath) : './webui/app'; var baseDir = config.baseDir ? path.resolve(config.baseDir, config.dataDirname) : getDataDir(config.dataDirname); var customDirs = path.join(baseDir, 'custom_dirs'); config.baseDir = baseDir; config.storage = config.storage && encodeURIComponent(config.storage); if (config.storage) { baseDir = path.join(customDirs, config.storage); } var shasum = crypto.createHash('sha1'); shasum.update(baseDir); config.baseDirHash = shasum.digest('hex'); config.rulesDir = path.join(baseDir, 'rules'); config.valuesDir = path.join(baseDir, 'values'); config.propertiesDir = path.join(baseDir, 'properties'); if (config.storage && newConf.copy) { var copyDir = typeof newConf.copy == 'string' && encodeURIComponent(newConf.copy); if (copyDir !== config.storage) { var dataDir = copyDir ? path.join(customDirs, copyDir) : baseDir; var rulesDir = path.join(dataDir, 'rules'); var valuesDir = path.join(dataDir, 'values'); var propsDir = path.join(dataDir, 'properties'); fse.ensureDirSync(rulesDir); fse.ensureDirSync(valuesDir); fse.ensureDirSync(propsDir); fse.copySync(rulesDir, config.rulesDir); fse.copySync(valuesDir, config.valuesDir); fse.copySync(propsDir, config.propertiesDir); } } return config; };