'use strict'; const http = require('http'), https = require('https'), async = require('async'), color = require('colorful'), certMgr = require('./lib/certMgr'), Recorder = require('./lib/recorder'), logUtil = require('./lib/log'), util = require('./lib/util'), events = require('events'), co = require('co'), WebInterface = require('./lib/webInterface'), ThrottleGroup = require('stream-throttle').ThrottleGroup; // const memwatch = require('memwatch-next'); // setInterval(() => { // console.log(process.memoryUsage()); // const rss = Math.ceil(process.memoryUsage().rss / 1000 / 1000); // console.log('Program is using ' + rss + ' mb of Heap.'); // }, 1000); // memwatch.on('stats', (info) => { // console.log('gc !!'); // console.log(process.memoryUsage()); // const rss = Math.ceil(process.memoryUsage().rss / 1000 / 1000); // console.log('GC !! Program is using ' + rss + ' mb of Heap.'); // // var heapUsed = Math.ceil(process.memoryUsage().heapUsed / 1000); // // console.log("Program is using " + heapUsed + " kb of Heap."); // // console.log(info); // }); const T_TYPE_HTTP = 'http', T_TYPE_HTTPS = 'https', DEFAULT_TYPE = T_TYPE_HTTP; const PROXY_STATUS_INIT = 'INIT'; const PROXY_STATUS_READY = 'READY'; const PROXY_STATUS_CLOSED = 'CLOSED'; /** * * @class ProxyCore * @extends {events.EventEmitter} */ class ProxyCore extends events.EventEmitter { /** * Creates an instance of ProxyCore. * * @param {object} config - configs * @param {number} config.port - port of the proxy server * @param {object} [config.rule=null] - rule module to use * @param {string} [config.type=http] - type of the proxy server, could be 'http' or 'https' * @param {strign} [config.hostname=localhost] - host name of the proxy server, required when this is an https proxy * @param {number} [config.throttle] - speed limit in kb/s * @param {boolean} [config.forceProxyHttps=false] - if proxy all https requests * @param {boolean} [config.silent=false] - if keep the console silent * @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response * @param {object} [config.recorder] - recorder to use * * @memberOf ProxyCore */ constructor(config) { super(); config = config || {}; this.status = PROXY_STATUS_INIT; this.proxyPort = config.port; this.proxyType = /https/i.test(config.type || DEFAULT_TYPE) ? T_TYPE_HTTPS : T_TYPE_HTTP; this.proxyHostName = config.hostname || 'localhost'; this.recorder = config.recorder; if (parseInt(process.versions.node.split('.')[0], 10) < 4) { throw new Error('node.js >= v4.x is required for anyproxy'); } else if (config.forceProxyHttps && !certMgr.ifRootCAFileExists()) { logUtil.printLog('You can run `anyproxy-ca` to generate one root CA and then re-run this command'); throw new Error('root CA not found. Please run `anyproxy-ca` to generate one first.'); } else if (this.proxyType === T_TYPE_HTTPS && !config.hostname) { throw new Error('hostname is required in https proxy'); } else if (!this.proxyPort) { throw new Error('proxy port is required'); } else if (!this.recorder) { throw new Error('recorder is required'); } else if (config.forceProxyHttps && config.rule && config.rule.beforeDealHttpsRequest) { logUtil.printLog('both "-i(--intercept)" and rule.beforeDealHttpsRequest are specified, the "-i" option will be ignored.', logUtil.T_WARN); config.forceProxyHttps = false; } this.httpProxyServer = null; this.requestHandler = null; // copy the rule to keep the original proxyRule independent this.proxyRule = config.rule || {}; if (config.silent) { logUtil.setPrintStatus(false); } if (config.throttle) { logUtil.printLog('throttle :' + config.throttle + 'kb/s'); const rate = parseInt(config.throttle, 10); if (rate < 1) { throw new Error('Invalid throttle rate value, should be positive integer'); } global._throttle = new ThrottleGroup({ rate: 1024 * rate }); // rate - byte/sec } // init recorder this.recorder = config.recorder; // init request handler const RequestHandler = util.freshRequire('./requestHandler'); this.requestHandler = new RequestHandler({ forceProxyHttps: !!config.forceProxyHttps, dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized }, this.proxyRule, this.recorder); } /** * manage all created socket * for each new socket, we put them to a map; * if the socket is closed itself, we remove it from the map * when the `close` method is called, we'll close the sockes before the server closed * * @param {Socket} the http socket that is creating * @returns undefined * @memberOf ProxyCore */ handleExistConnections(socket) { const self = this; self.socketIndex ++; const key = `socketIndex_${self.socketIndex}`; self.socketPool[key] = socket; // if the socket is closed already, removed it from pool socket.on('close', () => { delete self.socketPool[key]; }); } /** * start the proxy server * * @returns ProxyCore * * @memberOf ProxyCore */ start() { const self = this; self.socketIndex = 0; self.socketPool = {}; if (self.status !== PROXY_STATUS_INIT) { throw new Error('server status is not PROXY_STATUS_INIT, can not run start()'); } async.series( [ //creat proxy server function (callback) { if (self.proxyType === T_TYPE_HTTPS) { certMgr.getCertificate(self.proxyHostName, (err, keyContent, crtContent) => { if (err) { callback(err); } else { self.httpProxyServer = https.createServer({ key: keyContent, cert: crtContent }, self.requestHandler.userRequestHandler); callback(null); } }); } else { self.httpProxyServer = http.createServer(self.requestHandler.userRequestHandler); callback(null); } }, //handle CONNECT request for https over http function (callback) { self.httpProxyServer.on('connect', self.requestHandler.connectReqHandler); callback(null); }, function (callback) { // remember all sockets, so we can destory them when call the method 'close'; self.httpProxyServer.on('connection', (socket) => { self.handleExistConnections.call(self, socket); }); callback(null); }, //start proxy server function (callback) { self.httpProxyServer.listen(self.proxyPort); callback(null); }, ], //final callback (err, result) => { if (!err) { const tipText = (self.proxyType === T_TYPE_HTTP ? 'Http' : 'Https') + ' proxy started on port ' + self.proxyPort; logUtil.printLog(color.green(tipText)); if (self.webServerInstance) { const webTip = 'web interface started on port ' + self.webServerInstance.webPort; logUtil.printLog(color.green(webTip)); } let ruleSummaryString = ''; const ruleSummary = this.proxyRule.summary; if (ruleSummary) { co(function *() { if (typeof ruleSummary === 'string') { ruleSummaryString = ruleSummary; } else { ruleSummaryString = yield ruleSummary(); } logUtil.printLog(color.green(`Active rule is: ${ruleSummaryString}`)); }); } self.status = PROXY_STATUS_READY; self.emit('ready'); } else { const tipText = 'err when start proxy server :('; logUtil.printLog(color.red(tipText), logUtil.T_ERR); logUtil.printLog(err, logUtil.T_ERR); self.emit('error', { error: err }); } } ); return self; } /** * close the proxy server * * @returns ProxyCore * * @memberOf ProxyCore */ close() { // clear recorder cache return new Promise((resolve) => { if (this.httpProxyServer) { // destroy conns & cltSockets when closing proxy server for (const connItem of this.requestHandler.conns) { const key = connItem[0]; const conn = connItem[1]; logUtil.printLog(`destorying https connection : ${key}`); conn.end(); } for (const cltSocketItem of this.requestHandler.cltSockets) { const key = cltSocketItem[0]; const cltSocket = cltSocketItem[1]; logUtil.printLog(`endding https cltSocket : ${key}`); cltSocket.end(); } if (this.socketPool) { for (const key in this.socketPool) { this.socketPool[key].destroy(); } } this.httpProxyServer.close((error) => { if (error) { console.error(error); logUtil.printLog(`proxy server close FAILED : ${error.message}`, logUtil.T_ERR); } else { this.httpProxyServer = null; this.status = PROXY_STATUS_CLOSED; logUtil.printLog(`proxy server closed at ${this.proxyHostName}:${this.proxyPort}`); } resolve(error); }); } else { resolve(); } }) } } /** * start proxy server as well as recorder and webInterface */ class ProxyServer extends ProxyCore { /** * * @param {object} config - config * @param {object} [config.webInterface] - config of the web interface * @param {boolean} [config.webInterface.enable=false] - if web interface is enabled * @param {number} [config.webInterface.webPort=8002] - http port of the web interface * @param {number} [config.webInterface.wsPort] - web socket port of the web interface */ constructor(config) { // prepare a recorder const recorder = new Recorder(); const configForCore = Object.assign({ recorder, }, config); super(configForCore); this.proxyWebinterfaceConfig = config.webInterface; this.recorder = recorder; this.webServerInstance = null; } start() { // start web interface if neeeded if (this.proxyWebinterfaceConfig && this.proxyWebinterfaceConfig.enable) { this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, this.recorder); } new Promise((resolve) => { // start web server if (this.webServerInstance) { resolve(this.webServerInstance.start()); } else { resolve(null); } }) .then(() => { // start proxy core super.start() }) .catch((e) => { this.emit('error', e); }); } close() { return new Promise((resolve, reject) => { super.close() .then((error) => { if (error) { resolve(error); } }); if (this.recorder) { logUtil.printLog('clearing cache file...'); this.recorder.clear(); } const tmpWebServer = this.webServerInstance; this.recorder = null; this.webServerInstance = null; if (tmpWebServer) { logUtil.printLog('closing webserver...'); tmpWebServer.close((error) => { if (error) { console.error(error); logUtil.printLog(`proxy web server close FAILED: ${error.message}`, logUtil.T_ERR); } else { logUtil.printLog(`proxy web server closed at ${this.proxyHostName} : ${this.webPort}`); } resolve(error); }) } else { resolve(null); } }); } } module.exports.ProxyCore = ProxyCore; module.exports.ProxyServer = ProxyServer; module.exports.ProxyRecorder = Recorder; module.exports.ProxyWebServer = WebInterface; module.exports.utils = { systemProxyMgr: require('./lib/systemProxyMgr'), certMgr, };