diff --git a/lib/goldilocks.js b/lib/goldilocks.js index f714e28..c6108b8 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -7,76 +7,76 @@ module.exports.create = function (deps, config) { var PromiseA = require('bluebird'); var greenlock = require('greenlock'); var listeners = require('./servers').listeners; - var parseSni = require('sni'); - var modules = { }; - var program = { - tlsOptions: require('localhost.daplie.me-certificates').merge({}) - , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory' - , challengeType: 'tls-sni-01' - }; + var parseSni = require('sni'); + var modules = { }; + var program = { + tlsOptions: require('localhost.daplie.me-certificates').merge({}) + , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory' + , challengeType: 'tls-sni-01' + }; var secureContexts = {}; - var tunnelAdminTlsOpts = {}; - var tls = require('tls'); + var tunnelAdminTlsOpts = {}; + var tls = require('tls'); - var tcpRouter = { - _map: { } - , _create: function (address, port) { - // port provides hinting for http, smtp, etc - return function (conn, firstChunk, opts) { - console.log('[tcpRouter] ' + address + ':' + port + ' ' + (opts.servername || '')); + var tcpRouter = { + _map: { } + , _create: function (address, port) { + // port provides hinting for http, smtp, etc + return function (conn, firstChunk, opts) { + console.log('[tcpRouter] ' + address + ':' + port + ' ' + (opts.servername || '')); - var m; - var str; + var m; + var str; var hostname; var newHeads; - // TODO test per-module - // Maybe HTTP - if (firstChunk[0] > 32 && firstChunk[0] < 127) { - str = firstChunk.toString(); - m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); + // TODO test per-module + // Maybe HTTP + if (firstChunk[0] > 32 && firstChunk[0] < 127) { + str = firstChunk.toString(); + m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); hostname = (m && m[1].toLowerCase() || '').split(':')[0]; - console.log('[tcpRouter] hostname', hostname); - if (/HTTP\//i.test(str)) { - //conn.__service = 'http'; - } - } + console.log('[tcpRouter] hostname', hostname); + if (/HTTP\//i.test(str)) { + //conn.__service = 'http'; + } + } - if (!hostname) { - // TODO allow tcp tunneling - // TODO we need some way of tagging tcp as either terminated tls or insecure - conn.write( - "HTTP/1.1 404 Not Found\r\n" - + "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n" - + "Content-Type: text/html\r\n" - + "Content-Length: " + 9 + "\r\n" - + "\r\n" - + "Not Found" - ); + if (!hostname) { + // TODO allow tcp tunneling + // TODO we need some way of tagging tcp as either terminated tls or insecure + conn.write( + "HTTP/1.1 404 Not Found\r\n" + + "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n" + + "Content-Type: text/html\r\n" + + "Content-Length: " + 9 + "\r\n" + + "\r\n" + + "Not Found" + ); conn.end(); - return; - } + return; + } // Poor-man's http proxy // XXX SECURITY XXX: should strip existing X-Forwarded headers newHeads = [ "X-Forwarded-Proto: " + (opts.encrypted ? 'https' : 'http') - , "X-Forwarded-For: " + (conn.remoteAddress || opts.remoteAddress) + , "X-Forwarded-For: " + (opts.remoteAddress || conn.remoteAddress) , "X-Forwarded-Host: " + hostname ]; - if (!opts.encrypted) { + if (!opts.encrypted) { // a exists-only header that a bad client could not remove newHeads.push("X-Not-Encrypted: yes"); } - if (opts.servername) { + if (opts.servername) { newHeads.push("X-Forwarded-Sni: " + opts.servername); if (opts.servername !== hostname) { // an exists-only header that a bad client could not remove newHeads.push("X-Two-Servernames: yes"); } - } + } firstChunk = firstChunk.toString('utf8'); // JSON.stringify("Host: example.com\r\nNext: Header".replace(/(Host: [^\r\n]*)/i, "$1" + "\r\n" + "X: XYZ")) @@ -87,16 +87,16 @@ module.exports.create = function (deps, config) { // // hard-coded routes for the admin interface - if ( + if ( /\blocalhost\.admin\./.test(hostname) || /\badmin\.localhost\./.test(hostname) || /\blocalhost\.alpha\./.test(hostname) || /\balpha\.localhost\./.test(hostname) ) { - if (!modules.admin) { - modules.admin = require('./modules/admin.js').create(deps, config); - } - modules.admin.emit('connection', conn); - return; - } + if (!modules.admin) { + modules.admin = require('./modules/admin.js').create(deps, config); + } + modules.admin.emit('connection', conn); + return; + } // TODO static file handiling and such or whatever if (!modules.http) { @@ -105,35 +105,37 @@ module.exports.create = function (deps, config) { opts.hostname = hostname; conn.__opts = opts; modules.http.emit('connection', conn); - }; - } - , get: function getTcpRouter(address, port) { - address = address || '0.0.0.0'; + }; + } + , get: function getTcpRouter(address, port) { + address = address || '0.0.0.0'; - var id = address + ':' + port; - if (!tcpRouter._map[id]) { - tcpRouter._map[id] = tcpRouter._create(address, port); - } + var id = address + ':' + port; + if (!tcpRouter._map[id]) { + tcpRouter._map[id] = tcpRouter._create(address, port); + } - return tcpRouter._map[id]; - } - }; - var tlsRouter = { - _map: { } - , _create: function (address, port/*, nextServer*/) { - // port provides hinting for https, smtps, etc - return function (socket, firstChunk, opts) { + return tcpRouter._map[id]; + } + }; + var tlsRouter = { + _map: { } + , _create: function (address, port/*, nextServer*/) { + // port provides hinting for https, smtps, etc + return function (socket, firstChunk, opts) { var servername = opts.servername; - var packerStream = require('tunnel-packer').Stream; - var myDuplex = packerStream.create(socket); + var packerStream = require('tunnel-packer').Stream; + var myDuplex = packerStream.create(socket); - console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress); + myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress; + myDuplex.remotePort = opts.remotePort || myDuplex.remotePort; + console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress); - // needs to wind up in one of 3 states: - // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket) - // 2. Admin Interface (skips the proxying) - // 3. Terminated (goes on to a particular module or route) - //myDuplex.__tlsTerminated = true; + // needs to wind up in one of 3 states: + // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket) + // 2. Admin Interface (skips the proxying) + // 3. Terminated (goes on to a particular module or route) + //myDuplex.__tlsTerminated = true; process.nextTick(function () { // this must happen after the socket is emitted to the next in the chain, @@ -142,36 +144,36 @@ module.exports.create = function (deps, config) { }); // nextServer.emit could be used here - program.tlsTunnelServer.emit('connection', myDuplex); + program.tlsTunnelServer.emit('connection', myDuplex); // Why all this wacky-do with the myDuplex? // because https://github.com/nodejs/node/issues/8854, that's why - // (because node's internal networking layer == 💩 sometimes) - socket.on('data', function (chunk) { - console.log('[' + Date.now() + '] tls socket data', chunk.byteLength); - myDuplex.push(chunk); - }); - socket.on('error', function (err) { - console.error('[error] httpsTunnel (Admin) TODO close'); - console.error(err); - myDuplex.emit('error', err); - }); - socket.on('close', function () { - myDuplex.end(); - }); - }; - } - , get: function getTcpRouter(address, port) { - address = address || '0.0.0.0'; + // (because node's internal networking layer == 💩 sometimes) + socket.on('data', function (chunk) { + console.log('[' + Date.now() + '] tls socket data', chunk.byteLength); + myDuplex.push(chunk); + }); + socket.on('error', function (err) { + console.error('[error] httpsTunnel (Admin) TODO close'); + console.error(err); + myDuplex.emit('error', err); + }); + socket.on('close', function () { + myDuplex.end(); + }); + }; + } + , get: function getTcpRouter(address, port) { + address = address || '0.0.0.0'; - var id = address + ':' + port; - if (!tlsRouter._map[id]) { - tlsRouter._map[id] = tlsRouter._create(address, port); - } + var id = address + ':' + port; + if (!tlsRouter._map[id]) { + tlsRouter._map[id] = tlsRouter._create(address, port); + } - return tlsRouter._map[id]; - } - }; + return tlsRouter._map[id]; + } + }; // opts = { servername, encrypted, remoteAddress, remotePort } @@ -184,77 +186,77 @@ module.exports.create = function (deps, config) { // TODO port-based routing can do here - // TLS - if (22 === firstChunk[0]) { + // TLS + if (22 === firstChunk[0]) { servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid'; console.log('tryTls'); - tlsRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts); - } - else { + tlsRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts); + } + else { console.log('tryTcp'); - tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts); - } + tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, opts); + } }); } - function approveDomains(opts, certs, cb) { - // This is where you check your database and associated - // email addresses with domains and agreements and such + function approveDomains(opts, certs, cb) { + // This is where you check your database and associated + // email addresses with domains and agreements and such - // The domains being approved for the first time are listed in opts.domains - // Certs being renewed are listed in certs.altnames + // The domains being approved for the first time are listed in opts.domains + // Certs being renewed are listed in certs.altnames - function complete(err, stuff) { - opts.email = stuff.email; - opts.agreeTos = stuff.agreeTos; - opts.server = stuff.server; - opts.challengeType = stuff.challengeType; + function complete(err, stuff) { + opts.email = stuff.email; + opts.agreeTos = stuff.agreeTos; + opts.server = stuff.server; + opts.challengeType = stuff.challengeType; - cb(null, { options: opts, certs: certs }); - } + cb(null, { options: opts, certs: certs }); + } - if (certs) { - // TODO make sure the same options are used for renewal as for registration? - opts.domains = certs.altnames; + if (certs) { + // TODO make sure the same options are used for renewal as for registration? + opts.domains = certs.altnames; - cb(null, { options: opts, certs: certs }); - return; - } + cb(null, { options: opts, certs: certs }); + return; + } - // check config for domain name - if (-1 !== config.tls.servernames.indexOf(opts.domain)) { - // TODO how to handle SANs? - // TODO fetch domain-specific email - // TODO fetch domain-specific acmeDirectory - // NOTE: you can also change other options such as `challengeType` and `challenge` - // opts.challengeType = 'http-01'; - // opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet - complete(null, { - email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType }); - return; - } - // TODO ask http module about the default path (/srv/www/:hostname) - // (if it exists, we can allow and add to config) - if (!modules.http) { - modules.http = require('./modules/http.js').create(config); - } - modules.http.checkServername(opts.domain).then(function (stuff) { - if (!stuff.domains) { - // TODO once precheck is implemented we can just let it pass if it passes, yknow? - cb(new Error('domain is not allowed')); - return; - } + // check config for domain name + if (-1 !== config.tls.servernames.indexOf(opts.domain)) { + // TODO how to handle SANs? + // TODO fetch domain-specific email + // TODO fetch domain-specific acmeDirectory + // NOTE: you can also change other options such as `challengeType` and `challenge` + // opts.challengeType = 'http-01'; + // opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet + complete(null, { + email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType }); + return; + } + // TODO ask http module about the default path (/srv/www/:hostname) + // (if it exists, we can allow and add to config) + if (!modules.http) { + modules.http = require('./modules/http.js').create(config); + } + modules.http.checkServername(opts.domain).then(function (stuff) { + if (!stuff.domains) { + // TODO once precheck is implemented we can just let it pass if it passes, yknow? + cb(new Error('domain is not allowed')); + return; + } - complete(null, { - domain: stuff.domain || stuff.domains[0] - , domains: stuff.domains - , email: program.email - , server: program.acmeDirectoryUrl - , challengeType: program.challengeType - }); - return; - }, cb); - } + complete(null, { + domain: stuff.domain || stuff.domains[0] + , domains: stuff.domains + , email: program.email + , server: program.acmeDirectoryUrl + , challengeType: program.challengeType + }); + return; + }, cb); + } function getAcme() { return greenlock.create({ @@ -319,7 +321,7 @@ module.exports.create = function (deps, config) { // console.log('terminated data:', chunk.toString()); //}); //(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket); - //tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { encrypted: false }); + //tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { encrypted: false }); handler(tlsSocket, { servername: tlsSocket.servername , encrypted: true