From 7db2f0f70359302d7863b46a33a8cb75f30bf1a0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 24 May 2018 19:19:50 +0000 Subject: [PATCH] one step closer to the edge, but without the break --- README.md | 2 +- bin/telebitd.js | 130 ++++++++++++++++++++++++++-------------------- handlers.js | 80 +++++++++++++++++++++++++--- lib/unwrap-tls.js | 51 +++++++++++++----- 4 files changed, 188 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 09ef81f..70b600c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ for convenience. You can customize the installation: ```bash -export NODEJS_VER=v8.11.1 +export NODEJS_VER=v8.11.2 export TELEBITD_PATH=/opt/telebitd curl -fsS https://get.telebit.cloud/ | bash ``` diff --git a/bin/telebitd.js b/bin/telebitd.js index b3838e5..cc107b9 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -6,7 +6,7 @@ var pkg = require('../package.json'); var argv = process.argv.slice(2); var telebitd = require('../telebitd.js'); -var greenlock = require('greenlock'); +var Greenlock = require('greenlock'); var confIndex = argv.indexOf('--config'); var confpath; @@ -55,6 +55,80 @@ function applyConfig(config) { console.info(""); console.info(""); } + + function approveDomains(opts, certs, cb) { + console.log('[debug] approveDomains', opts.domains); + // 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 + if (certs) { + opts.domains = certs.altnames; + cb(null, { options: opts, certs: certs }); + return; + } + + if (state.config.vhost) { + console.log('[sni] vhost checking is turned on'); + var vhost = state.config.vhost.replace(/:hostname/, opts.domains[0]); + require('fs').readdir(vhost, function (err, nodes) { + console.log('[sni] checking fs vhost'); + if (err) { check(); return; } + if (nodes) { approve(); } + }); + return; + } + + function approve() { + opts.email = state.config.email; + opts.agreeTos = state.config.agreeTos; + opts.challenges = { + // TODO dns-01 + 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) + }; + opts.communityMember = state.config.communityMember; + cb(null, { options: opts, certs: certs }); + } + + function check() { + console.log('[sni] checking servername'); + if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) { + approve(); + } else { + cb(new Error("failed the approval chain '" + opts.domains[0] + "'")); + } + console.log('Approve Domains cb'); + } + check(); + } + +/* + if (!config.email || !config.agreeTos) { + console.error("You didn't specify --email and --agree-tos"); + console.error("(required for ACME / Let's Encrypt / Greenlock TLS/SSL certs)"); + console.error(""); + process.exit(1); + } +*/ + + state.greenlock = Greenlock.create({ + + version: 'draft-11' + , server: 'https://acme-v02.api.letsencrypt.org/directory' + //, server: 'https://acme-staging-v02.api.letsencrypt.org/directory' + + , store: require('le-store-certbot').create({ debug: true, webrootPath: '/tmp/acme-challenges' }) + + , approveDomains: approveDomains + + , configDir: '/root/acme' + , debug: true + + //, approvedDomains: program.servernames + + }); + require('../handlers').create(state); // adds directly to config for now... //require('cluster-store').create().then(function (store) { @@ -77,60 +151,6 @@ function applyConfig(config) { state.tcp[port].on('connection', netConnHandlers.tcp); }); //}); - - function approveDomains(opts, certs, cb) { - console.log('Approve Domains', opts.domains); - // 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 - if (certs) { - opts.domains = certs.altnames; - } else { - if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) { - opts.email = state.config.email; - opts.agreeTos = state.config.agreeTos; - opts.challenges = { - // TODO dns-01 - 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) - }; - opts.communityMember = state.config.communityMember; - } - } - - // NOTE: you can also change other options such as `challengeType` and `challenge` - // opts.challengeType = 'http-01'; - // opts.challenge = require('le-challenge-fs').create({}); - - console.log('Approve Domains cb'); - cb(null, { options: opts, certs: certs }); - } - -/* - if (!config.email || !config.agreeTos) { - console.error("You didn't specify --email and --agree-tos"); - console.error("(required for ACME / Let's Encrypt / Greenlock TLS/SSL certs)"); - console.error(""); - process.exit(1); - } -*/ - - state.greenlock = greenlock.create({ - - version: 'draft-11' - , server: 'https://acme-staging-v02.api.letsencrypt.org/directory' - - , store: require('le-store-certbot').create({ debug: true, webrootPath: '/tmp/acme-challenges' }) - - , approveDomains: approveDomains - - , configDir: '/root/acme' - , debug: true - - //, approvedDomains: program.servernames - - }); } require('fs').readFile(confpath, 'utf8', function (err, text) { diff --git a/handlers.js b/handlers.js index 8ca03c3..82c0788 100644 --- a/handlers.js +++ b/handlers.js @@ -5,6 +5,14 @@ var tls = require('tls'); var wrapSocket = require('tunnel-packer').wrapSocket; var redirectHttps = require('redirect-https')(); +function noSniCallback(tag) { + return function _noSniCallback(servername, cb) { + var err = new Error("[noSniCallback] no handler set for '" + tag + "':'" + servername + "'"); + console.error(err.message); + cb(new Error(err)); + } +} + module.exports.create = function (state) { var tunnelAdminTlsOpts = {}; var setupSniCallback; @@ -62,6 +70,9 @@ module.exports.create = function (state) { // things get a little messed up here state.httpInvalidSniServer.emit('connection', tlsSocket); }); + state.tlsInvalidSniServer.on('tlsClientError', function () { + console.error('tlsClientError InvalidSniServer'); + }); state.httpsInvalid = function (servername, socket) { // none of these methods work: // httpsServer.emit('connection', socket); // this didn't work @@ -95,6 +106,9 @@ module.exports.create = function (state) { }); httpInvalidSniServer.emit('connection', tlsSocket); }); + tlsInvalidSniServer.on('tlsClientError', function () { + console.error('tlsClientError InvalidSniServer httpsInvalid'); + }); tlsInvalidSniServer.emit('connection', wrapSocket(socket)); }; @@ -110,15 +124,24 @@ module.exports.create = function (state) { Object.keys(state.tlsOptions).forEach(function (key) { tunnelAdminTlsOpts[key] = state.tlsOptions[key]; }); - tunnelAdminTlsOpts.SNICallback = (state.greenlock && state.greenlock.tlsOptions && function (servername, cb) { - console.log("time to handle '" + servername + "'"); - state.greenlock.tlsOptions.SNICallback(servername, cb); - }) || tunnelAdminTlsOpts.SNICallback; + if (state.greenlock && state.greenlock.tlsOptions) { + console.log('greenlock tlsOptions for SNICallback'); + tunnelAdminTlsOpts.SNICallback = function (servername, cb) { + console.log("time to handle '" + servername + "'"); + state.greenlock.tlsOptions.SNICallback(servername, cb); + }; + } else { + console.log('custom or null tlsOptions for SNICallback'); + tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin'); + } state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { - console.log('tls connection'); + console.log('(Admin) tls connection'); // things get a little messed up here (state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket); }); + state.tlsTunnelServer.on('tlsClientError', function () { + console.error('tlsClientError TunnelServer client error'); + }); state.httpsTunnel = function (servername, socket) { // none of these methods work: // httpsServer.emit('connection', socket); // this didn't work @@ -152,11 +175,54 @@ module.exports.create = function (state) { // things get a little messed up here state.httpSetupServer.emit('connection', tlsSocket); }); + state.tlsSetupServer.on('tlsClientError', function () { + console.error('tlsClientError SetupServer'); + }); state.httpsSetupServer = function (servername, socket) { - console.log('httpsTunnel (Admin) servername', servername); + console.log('httpsTunnel (Setup) servername', servername); state._servernames = [servername]; state.config.agreeTos = true; // TODO: BUG XXX BAD, make user accept - setupSniCallback = state.greenlock.tlsOptions.SNICallback; + setupSniCallback = state.greenlock.tlsOptions.SNICallback || noSniCallback('setup'); state.tlsSetupServer.emit('connection', wrapSocket(socket)); }; + + // + // vhost + // + state.httpVhost = http.createServer(function (req, res) { + console.log('httpVhost (local)'); + console.log('req.socket.encrypted', req.socket.encrypted); + + var finalhandler = require('finalhandler'); + // TODO compare SNI to hostname? + var host = (req.headers.host||'').toLowerCase().trim(); + var serveSetup = require('serve-static')(state.config.vhost.replace(/:hostname/g, host), { redirect: true }); + + if (req.socket.encrypted) { serveSetup(req, res, finalhandler(req, res)); return; } + + console.log('try greenlock middleware for vhost'); + (state.greenlock && state.greenlock.middleware(redirectHttpsAndClose) + || redirectHttpsAndClose)(req, res, function () { + console.log('fallthrough to vhost serving???'); + serveSetup(req, res, finalhandler(req, res)); + }); + }); + state.tlsVhost = tls.createServer( + { SNICallback: function (servername, cb) { + console.log('tlsVhost debug SNICallback', servername); + tunnelAdminTlsOpts.SNICallback(servername, cb); + } + } + , function (tlsSocket) { + console.log('tlsVhost (local)'); + state.httpVhost.emit('connection', tlsSocket); + } + ); + state.tlsVhost.on('tlsClientError', function () { + console.error('tlsClientError Vhost'); + }); + state.httpsVhost = function (servername, socket) { + console.log('httpsVhost (local)', servername); + state.tlsVhost.emit('connection', wrapSocket(socket)); + }; }; diff --git a/lib/unwrap-tls.js b/lib/unwrap-tls.js index 0986b2a..11a1b3a 100644 --- a/lib/unwrap-tls.js +++ b/lib/unwrap-tls.js @@ -80,6 +80,10 @@ module.exports.createTcpConnectionHandler = function (copts) { var m; function tryTls() { + var vhost; + + console.log(""); + if (!copts.servernames.length) { console.log("https => admin => setup => (needs bogus tls certs to start?)"); copts.httpsSetupServer(servername, conn); @@ -92,21 +96,44 @@ module.exports.createTcpConnectionHandler = function (copts) { return; } - if (!servername) { - console.log("No SNI was given, so there's nothing we can do here"); - copts.httpsInvalid(servername, conn); + if (copts.config.nowww && /^www\./i.test(servername)) { + console.log("TODO: use www bare redirect"); + } + + function run() { + if (!servername) { + console.log("No SNI was given, so there's nothing we can do here"); + copts.httpsInvalid(servername, conn); + return; + } + + var nextDevice = Devices.next(copts.deviceLists, servername); + if (!nextDevice) { + console.log("No devices match the given servername"); + copts.httpsInvalid(servername, conn); + return; + } + + console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])"); + pipeWs(servername, service, conn, nextDevice); + } + + if (copts.config.vhost) { + console.log("VHOST path", copts.config.vhost); + vhost = copts.config.vhost.replace(/:hostname/, (servername||'')); + console.log("VHOST name", vhost); + conn.pause(); + //copts.httpsVhost(servername, conn); + //return; + require('fs').readdir(vhost, function (err, nodes) { + console.log("VHOST error?", err); + if (err) { run(); return; } + if (nodes) { copts.httpsVhost(servername, conn); } + }); return; } - var nextDevice = Devices.next(copts.deviceLists, servername); - if (!nextDevice) { - console.log("No devices match the given servername"); - copts.httpsInvalid(servername, conn); - return; - } - - console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])"); - pipeWs(servername, service, conn, nextDevice); + run(); } // https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155