http-simple-proxy
Version:
Simple http/https proxy daemon. Proxying based on the excellent node-http-master.
437 lines (367 loc) • 11.8 kB
JavaScript
;
var crypto = require('crypto');
var net = require('net');
var http = require('http');
var async = require('async');
var regexpQuote = require('./DispatchTable').regexpQuote;
var tls = require('tls');
var DI = require('./di');
var path = require('path');
var extend = require('extend');
var ocsp = require('ocsp');
let Promise = require('bluebird');
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
var EventEmitter = require('eventemitter3');
var punycode = require('punycode');
var createCredentials;
if (tls.createSecureContext) {
createCredentials = tls.createSecureContext;
} else {
createCredentials = crypto.createCredentials;
}
function lazyGetTcpServer(port, host, cb) {
var tcpServers = this.tcpServers;
var entry = (host ? host + ':' + port : port);
if (tcpServers[entry]) {
cb(null, tcpServers[entry]);
} else {
var tcpServer = tcpServers[entry] = net.createServer();
var handler = function(err) {
if (err) return cb(err);
cb(null, tcpServer);
};
try {
if (host)
tcpServer.listen(port, host, handler);
else
tcpServer.listen(port, handler);
tcpServer.once('error', function(err) {
delete tcpServers[entry];
cb(err);
});
} catch (err) {
cb(err);
}
}
}
function loadKeysforConfigEntry(config, callback) {
callback();
}
function handlePortEntryConfig(host, portNumber, portEntryConfig, callback) {
loadKeysforConfigEntry.call(this, portEntryConfig, err => {
if (err) {
return callback(err);
}
handleConfigEntryAfterLoadingKeys.call(this, host, portNumber, portEntryConfig, callback);
});
}
function patchSslConfig(portEntrySslConfig) {
if (nodeVersion >= 0.11) { // use fancy cipher settings only for 0.11
if (portEntrySslConfig.honorCipherOrder !== false) {
// prefer server ciphers over clients - prevents BEAST attack
portEntrySslConfig.honorCipherOrder = true;
}
if (!portEntrySslConfig.ciphers) {
portEntrySslConfig.ciphers = 'EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+AES+SHA:EECDH+aRSA+RC4:EECDH:EDH+aRSA:RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS::+RC4:RC4';
if (portEntrySslConfig.disableWeakCiphers) {
portEntrySslConfig.ciphers += ':!RC4';
}
} else if (portEntrySslConfig.disableWeakCiphers) {
this.logNotice('disableWeakCiphers is incompatible with pre-set cipher list');
}
} else if (portEntrySslConfig.disableWeakCiphers) {
this.logNotice('disableWeakCiphers is unsupported for node 0.10');
}
}
function createHandlers(portNumber, portConfig) {
var self = this;
var di = this.di.makeChild();
di.bindInstance('di', di);
di.bindInstance('portConfig', portConfig);
di.bindInstance('portNumber', portNumber);
di.onMissing = function(name) {
var m;
if ((m = name.match(/(.+)Middleware$/))) {
name = m[1];
try {
di.bindType(name + 'Middleware', require('../' + path.join('modules/middleware/', name)));
} catch (err) {
console.log(err && err.message);
return;
}
return di.resolve(name + 'Middleware');
}
};
var router = di.resolve('routerMiddleware');
// allow also for specifying 80: 'http://code2flow.com:8080'
if (typeof portConfig !== 'object' || portConfig instanceof Array) {
portConfig = {
router: portConfig
};
}
if (!(portConfig.router instanceof Array)) {
portConfig.router = [portConfig.router];
}
portConfig.router = [].concat(self.middleware)
.concat(self.config.middleware || [])
.concat(portConfig.middleware || [])
.concat(portConfig.router);
var reject = di.resolve('rejectMiddleware');
var target = router.entryParser(portConfig.router);
return {
request: function(req, res, next) {
router.requestHandler(req, res, next, target);
},
error: function(err, req, res) {
if(err)
self.logNotice('Request error. '+err.toString())
var code = 500;
if (!err)
code = 503;
reject.requestHandler(req, res, null, reject.entryParser(500));
}
};
}
function serverForPortConfig(host, portNumber, portConfig) {
var self = this;
var server;
self.cachedServers = self.cachedServers || {};
var key = (host ? host + ':' + portNumber : portNumber);
var sslCachedConfig = extend({}, portConfig.ssl);
var cached = self.cachedServers[key];
if (cached) {
server = self.cachedServers[key].server;
server.removeAllListeners();
if (JSON.stringify(sslCachedConfig) === cached.sslConfig) {
return server;
}
}
if (portConfig.ssl) {
var https = require('https');
server = https.createServer(portConfig.ssl);
} else {
server = http.createServer();
}
self.cachedServers[key] = {
server: server,
sslConfig: JSON.stringify(sslCachedConfig)
}
return server;
}
function handleConfigEntryAfterLoadingKeys(host, portNumber, config, callback) {
var self = this;
var handlers = createHandlers.call(this, portNumber, config);
var handler = require('./requestHandler')(handlers.request, handlers.error);
var server;
try {
server = serverForPortConfig.call(this, host, portNumber, config);
server.removeAllListeners('request');
server.on('request', handler);
} catch (err) {
return callback(err, null);
}
function listeningHandler() {
server.removeAllListeners('error'); // remove the below handler
callback(null, server);
server.removeListener('error', errorHandler);
}
function errorHandler(err) {
server.removeAllListeners('listening'); // remove the above handler
callback(err, server);
server.removeListener('listening', listeningHandler);
}
server.once('listening', listeningHandler);
server.once('error', errorHandler);
server.removeAllListeners('upgrade');
server.on('upgrade', function(req, socket, head) {
req.upgrade = {
socket: socket,
head: head
};
handler(req, { // fake res object for log middleware to work
socket: socket
});
});
lazyGetTcpServer.call(self, portNumber, host, function(err, tcpServer) {
if (err) return callback(err, null);
tcpServer.removeAllListeners();
tcpServer.on('connection', function(socket) {
server.emit('connection', socket);
});
tcpServer.on('error', function(err) {
server.emit('error', err);
});
tcpServer.on('close', function(err) {
server.emit('close');
});
server.emit('listening');
});
}
function handleConfig(config, configHandled) {
var self = this;
self.config = config;
var errors = {};
if (config.disableTlsClientRenegotiation) {
tls.CLIENT_RENEG_LIMIT = 0;
tls.CLIENT_RENEG_WINDOW = 0;
}
async.parallel(Object.keys(config.ports || {}).map(function(portEntry) {
return function(asyncCallback) {
var m;
// TODO: IPV6?
if ((m = portEntry.match(/((\S+):)?(\d+)/))) {
var listenHost = m[2];
var listenPortNumber = parseInt(m[3]);
var portConfig = config.ports[portEntry];
handlePortEntryConfig.call(self, listenHost, listenPortNumber, portConfig, function(err, server) {
var entryString = (listenHost ? listenHost + ':' + listenPortNumber : 'port ' + listenPortNumber);
if (err) {
self.logError('Error while starting entry ' + entryString + ' : ' + err.toString());
if (err.stack)
self.logError(err.stack);
errors[portEntry] = err;
}
if (server) {
self.logNotice('Listening on port: ' + entryString);
} else
self.logNotice('Entry ' + entryString + ' is unusable');
// ignore error to not crash the entire proxy
asyncCallback(null, server);
});
}
};
}), function(err, results) {
// if (err) {
// return configHandled(err);
// }
self.logNotice('Start successful');
self.servers = results.filter(function(server) {
return !!server;
});
if (Object.keys(errors).length == 0) {
configHandled(null);
} else {
configHandled(errors);
}
});
}
function unbindAll(cb) {
this.servers.forEach(function(server) {
server.unref();
});
this.servers = [];
var self = this;
Object.keys(this.tcpServers).forEach(function(key) {
self.tcpServers[key].removeAllListeners();
});
cb();
}
function HttpSimpleProxyWorker(config) {
config = config || {};
this.config = config;
this.middleware = [];
var store = {};
this.tlsSessionStore = config.tlsSessionStore || {
get: function(id, cb) {
id = id.toString('base64');
cb(null, store[id], null);
},
set: function(id, data, cb) {
id = id.toString('base64');
store[id] = data;
// todo cleanup old ids
if (cb)
cb();
}
};
this.tcpServers = {};
this.servers = [];
}
HttpSimpleProxyWorker.prototype = Object.create(EventEmitter.prototype);
HttpSimpleProxyWorker.prototype.logNotice = function(msg) {
this.emit('logNotice', msg);
};
HttpSimpleProxyWorker.prototype.logError = function(msg) {
this.emit('logError', msg);
};
HttpSimpleProxyWorker.prototype.unbindAll = function(unbindFinished) {
unbindAll.call(this, unbindFinished);
};
HttpSimpleProxyWorker.prototype.loadConfig = function(config, configLoaded) {
var self = this;
var events = new EventEmitter();
this.config = config;
function messageHandler(msg) {
events.emit('msg:' + msg.type, msg.data, msg.workerId);
}
this.handleMessage = messageHandler;
this.unbindAll(function() {});
if (this.di) {
this.emit('reload');
}
var di = this.di = new DI();
di.onMissing = function(name) {
var m;
if ((m = name.match(/(.+)Service$/))) {
name = m[1];
try {
this.bindType(name + 'Service', require(path.join(__dirname, '..', 'modules/services/', name)));
} catch (err) {
console.log(err && err.message);
return;
}
self.emit('loadService', name);
return this.resolve(name + 'Service');
}
};
di.bindInstance('di', di);
di.bindInstance('worker', this);
this.once('reload', function() {
process.removeListener('msg', messageHandler);
events.emit('reload');
events.removeAllListeners();
});
di.bindInstance('events', events);
di.bindResolver('config', function() {
return self.config;
});
di.bindInstance('master', null);
Object.keys(config.modules || {}).forEach(function(moduleName) {
if (!config.modules[moduleName])
return;
var di = self.di.makeChild();
di.bindInstance('di', di);
di.bindInstance('moduleConfig', config.modules[moduleName]);
try {
di.resolve(require(path.join(__dirname, '..', 'modules', moduleName)));
} catch (err) {
console.error("Error loading module:", moduleName, err);
}
});
handleConfig.call(this, config, function(err) {
self.gcServers(function() {
if (configLoaded)
configLoaded(err);
});
});
};
HttpSimpleProxyWorker.prototype.gcServers = function(gcFinished) {
var self = this;
var toClose = [];
Object.keys(this.tcpServers).forEach(function(key) {
var server = self.tcpServers[key];
if (require('events').EventEmitter.listenerCount(server, 'connection') === 0) {
toClose.push(server);
delete self.tcpServers[key];
if (self.cachedServers[key]) {
self.cachedServers[key].server.removeAllListeners();
delete self.cachedServers[key];
}
}
});
async.each(toClose, function(server, cb) {
server.close();
cb();
}, gcFinished);
};
module.exports = HttpSimpleProxyWorker;