From 4fb20d28ce0ae099b087fea81dbc93589fae4b2f Mon Sep 17 00:00:00 2001 From: "xiaofeng.mxf" Date: Thu, 23 Jan 2020 11:02:23 +0800 Subject: [PATCH] fix https proxy server for ip host --- jest.config.js | 1 + lib/httpsServerMgr.js | 179 +++++++++++++++++--------------- lib/util.js | 2 +- test/lib/httpsServerMgr.spec.js | 68 ++++++++++-- 4 files changed, 154 insertions(+), 96 deletions(-) diff --git a/jest.config.js b/jest.config.js index 5081a7f..b9eb114 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,7 @@ // https://jestjs.io/docs/en/configuration.html module.exports = { + testTimeout: 10 * 1000, // All imported modules in your tests should be mocked automatically // automock: false, diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js index 2b39db0..d1d162b 100644 --- a/lib/httpsServerMgr.js +++ b/lib/httpsServerMgr.js @@ -4,61 +4,16 @@ const async = require('async'), https = require('https'), tls = require('tls'), + assert = require('assert'), crypto = require('crypto'), color = require('colorful'), certMgr = require('./certMgr'), logUtil = require('./log'), util = require('./util'), wsServerMgr = require('./wsServerMgr'), - co = require('co'), - assert = require('assert'), constants = require('constants'), asyncTask = require('async-task-mgr'); -const createSecureContext = tls.createSecureContext || crypto.createSecureContext; -function SNIPrepareCert(serverName, SNICallback) { - let keyContent, - crtContent, - ctx; - - async.series([ - (callback) => { - certMgr.getCertificate(serverName, (err, key, crt) => { - if (err) { - callback(err); - } else { - keyContent = key; - crtContent = crt; - callback(); - } - }); - }, - (callback) => { - try { - ctx = createSecureContext({ - key: keyContent, - cert: crtContent - }); - callback(); - } catch (e) { - callback(e); - } - } - ], (err) => { - if (!err) { - const tipText = 'proxy server for __NAME established'.replace('__NAME', serverName); - logUtil.printLog(color.yellow(color.bold('[internal https]')) + color.yellow(tipText)); - SNICallback(null, ctx); - } else { - logUtil.printLog('err occurred when prepare certs for SNI - ' + err, logUtil.T_ERR); - logUtil.printLog('err occurred when prepare certs for SNI - ' + err.stack, logUtil.T_ERR); - } - }); -} - -//config.port - port to start https server -//config.handler - request handler - /** * Create an https server * @@ -66,55 +21,107 @@ function SNIPrepareCert(serverName, SNICallback) { * @param {number} config.port * @param {function} config.handler */ -function createHttpsServer(config) { - if (!config || !config.port || !config.handler) { - throw (new Error('please assign a port')); +function createHttpsSNIServer(port, handler) { + assert(port && handler, 'invalid param for https SNI server'); + + const createSecureContext = tls.createSecureContext || crypto.createSecureContext; + function SNIPrepareCert(serverName, SNICallback) { + let keyContent, + crtContent, + ctx; + + async.series([ + (callback) => { + certMgr.getCertificate(serverName, (err, key, crt) => { + if (err) { + callback(err); + } else { + keyContent = key; + crtContent = crt; + callback(); + } + }); + }, + (callback) => { + try { + ctx = createSecureContext({ + key: keyContent, + cert: crtContent + }); + callback(); + } catch (e) { + callback(e); + } + } + ], (err) => { + if (!err) { + const tipText = 'proxy server for __NAME established'.replace('__NAME', serverName); + logUtil.printLog(color.yellow(color.bold('[internal https]')) + color.yellow(tipText)); + SNICallback(null, ctx); + } else { + logUtil.printLog('err occurred when prepare certs for SNI - ' + err, logUtil.T_ERR); + logUtil.printLog('err occurred when prepare certs for SNI - ' + err.stack, logUtil.T_ERR); + SNICallback(err); + } + }); } return new Promise((resolve) => { const server = https.createServer({ secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, SNICallback: SNIPrepareCert, - }, config.handler).listen(config.port); + }, handler).listen(port); resolve(server); }); } -/** - * - * - * @class httpsServerMgr - * @param {object} config - * @param {function} config.handler handler to deal https request - * - */ +function createHttpsIPServer(ip, port, handler) { + assert(ip && port && handler, 'invalid param for https IP server'); + + return new Promise((resolve, reject) => { + certMgr.getCertificate(ip, (err, keyContent, crtContent) => { + if (err) return reject(err); + const server = https.createServer({ + secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, + key: keyContent, + cert: crtContent, + }, handler).listen(port); + + resolve(server); + }); + }); +} + class httpsServerMgr { constructor(config) { - assert(config, 'config is required'); - assert(config.handler && config.wsHandler, 'handler and wsHandler are required'); - assert(config.hostname, 'hostname is required'); - this.hostname = config.hostname; - this.handler = config.handler; - this.wsHandler = config.wsHandler; + if (!config || !config.handler) { + throw new Error('handler is required'); + } this.httpsAsyncTask = new asyncTask(); - this.asyncTaskName = `https_${Math.random()}`; - this.httpsServer = null; + this.handler = config.handler; + this.wsHandler = config.wsHandler + this.asyncSNITaskName = `https_SNI_${Math.random()}`; + this.activeServers = []; } - getSharedHttpsServer() { + getSharedHttpsServer(hostname) { const self = this; - const finalHost = self.hostname; - function prepareServer(callback) { - let instancePort; - co(util.getFreePort) - .then(co.wrap(function *(port) { - instancePort = port; - let httpsServer = null; + const ifIPHost = hostname && util.isIp(hostname); + const serverHost = '127.0.0.1'; - httpsServer = yield createHttpsServer({ - port, - handler: self.handler - }); + function prepareServer(callback) { + let port; + Promise.resolve(util.getFreePort()) + .then(freePort => { + port = freePort; + if (ifIPHost) { + return createHttpsIPServer(hostname, port, self.handler); + } else { + return createHttpsSNIServer(port, self.handler); + } + }) + .then(httpsServer => { + self.activeServers.push(httpsServer); wsServerMgr.getWsServer({ server: httpsServer, @@ -125,22 +132,20 @@ class httpsServerMgr { logUtil.debug('will let WebSocket server to handle the upgrade event'); }); - self.httpsServer = httpsServer; - const result = { - host: finalHost, - port: instancePort, + host: serverHost, + port, }; callback(null, result); - return result; - })) + }) .catch(e => { callback(e); }); } + // same server for same host return new Promise((resolve, reject) => { - self.httpsAsyncTask.addTask(self.asyncTaskName, prepareServer, (error, serverInfo) => { + self.httpsAsyncTask.addTask(ifIPHost ? hostname : serverHost, prepareServer, (error, serverInfo) => { if (error) { reject(error); } else { @@ -151,7 +156,9 @@ class httpsServerMgr { } close() { - return this.httpsServer && this.httpsServer.close(); + this.activeServers.forEach(server => { + server.close(); + }); } } diff --git a/lib/util.js b/lib/util.js index bfe9644..d06536b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -298,7 +298,7 @@ module.exports.getByteSize = function (content) { /* * identify whether the */ -module.exports.isIpDomain = function (domain) { +module.exports.isIp = function (domain) { if (!domain) { return false; } diff --git a/test/lib/httpsServerMgr.spec.js b/test/lib/httpsServerMgr.spec.js index c169250..f4b5f78 100644 --- a/test/lib/httpsServerMgr.spec.js +++ b/test/lib/httpsServerMgr.spec.js @@ -1,17 +1,67 @@ +const tls = require('tls'); const httpsServerMgr = require('../../lib/httpsServerMgr'); describe('httpsServerMgr', () => { - it('get https server', async () => { - const serverMgr = new httpsServerMgr({ + let serverMgrInstance; + + beforeAll(async () => { + serverMgrInstance = new httpsServerMgr({ hostname: '127.0.0.1', - handler: () => { - console.log('this is handler'); - }, - wsHandler: () => { - console.log('this is handler'); + handler: (req, res) => { + res.end('hello world'); }, + wsHandler: () => { }, + }); + }); + + afterAll(async () => { + await serverMgrInstance.close(); + }); + + it('SNI server should work properly', async () => { + const sniServerA = await serverMgrInstance.getSharedHttpsServer('a.anyproxy.io'); + const sniServerB = await serverMgrInstance.getSharedHttpsServer('b.anyproxy.io'); + + expect(sniServerA).toEqual(sniServerB); // SNI - common server + + const connectHostname = 'some_new_host.anyproxy.io'; + const connectOpt = { + servername: connectHostname, // servername is required for sni server + rejectUnauthorized: false, + } + await new Promise((resolve, reject) => { + const socketToSNIServer = tls.connect(sniServerA.port, '127.0.0.1', connectOpt, (tlsSocket) => { + // console.log('client to SNI server connected, ', socketToSNIServer.authorized ? 'authorized' : 'unauthorized'); + const certSubject = socketToSNIServer.getPeerCertificate().subject; + expect(certSubject.CN).toEqual(connectHostname); + socketToSNIServer.end(); + resolve(); + }); + + socketToSNIServer.on('keylog', line => { + console.log(line); + }) + }); + }); + + it('IP server should work properly', async () => { + const ipServerHost = '1.2.3.4'; + const anotherSNIServer = await serverMgrInstance.getSharedHttpsServer('c.anyproxy.io'); + const ipServerA = await serverMgrInstance.getSharedHttpsServer(ipServerHost); + const ipServerB = await serverMgrInstance.getSharedHttpsServer('5.6.7.8'); + expect(ipServerA).not.toEqual(ipServerB); + expect(anotherSNIServer).not.toEqual(ipServerA); + + const connectOpt = { + rejectUnauthorized: false, + } + await new Promise((resolve, reject) => { + const socketToIpServer = tls.connect(ipServerA.port, '127.0.0.1', connectOpt, () => { + const certSubject = socketToIpServer.getPeerCertificate().subject; + expect(certSubject.CN).toEqual(ipServerHost); + socketToIpServer.end(); + resolve(); + }); }); - await serverMgr.getSharedHttpsServer(); - serverMgr.close(); }); });