diff --git a/bin/telebit.js b/bin/telebit.js index 5dc76e5..9d026fd 100755 --- a/bin/telebit.js +++ b/bin/telebit.js @@ -3,6 +3,7 @@ 'use strict'; var pkg = require('../package.json'); +console.log(pkg.name, pkg.version); var url = require('url'); var remote = require('../remote.js'); @@ -37,7 +38,12 @@ function help() { process.exit(0); } -if (-1 === confIndex || -1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) { +if (-1 === confIndex) { + confpath = require('path').join(require('os').homedir(), '.config/telebit/telebit.yml'); + console.info('Using default --config "' + confpath + '"'); +} + +if (-1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) { help(); } if (!confpath || /^--/.test(confpath)) { @@ -74,7 +80,6 @@ require('fs').readFile(confpath, 'utf8', function (err, text) { }); function connectTunnel() { - var services = { https: {}, http: {}, tcp: {} }; state.net = { createConnection: function (info, cb) { // data is the hello packet / first chunk @@ -86,66 +91,17 @@ function connectTunnel() { } }; - // Note: the remote needs to know: - // what servernames to forward - // what ports to forward - // what udp ports to forward - // redirect http to https automatically - // redirect www to nowww automatically - if (state.config.http) { - Object.keys(state.config.http).forEach(function (hostname) { - if ('*' === hostname) { - state.config.servernames.forEach(function (servername) { - services.https[servername] = state.config.http[hostname]; - services.http[servername] = 'redirect-https'; - }); - return; - } - services.https[hostname] = state.config.http[hostname]; - services.http[hostname] = 'redirect-https'; - }); - } - /* - Object.keys(state.config.localPorts).forEach(function (port) { - var proto = state.config.localPorts[port]; - if (!proto) { return; } - if ('http' === proto) { - state.config.servernames.forEach(function (servername) { - services.http[servername] = port; - }); - return; - } - if ('https' === proto) { - state.config.servernames.forEach(function (servername) { - services.https[servername] = port; - }); - return; - } - if (true === proto) { proto = 'tcp'; } - if ('tcp' !== proto) { throw new Error("unsupported protocol '" + proto + "'"); } - //services[proxy.protocol]['*'] = proxy.port; - //services[proxy.protocol][proxy.hostname] = proxy.port; - services[proto]['*'] = port; - }); - */ - state.services = services; - - Object.keys(services).forEach(function (protocol) { - var subServices = state.services[protocol]; - Object.keys(subServices).forEach(function (hostname) { - console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]); - }); - }); - console.info(''); - state.greenlock = state.config.greenlock || {}; + if (!state.config.sortingHat) { + state.config.sortingHat = './lib/sorting-hat.js'; + } + state.config.sortingHat = require('path').resolve(__dirname, '..', state.config.sortingHat); + // TODO Check undefined vs false for greenlock config var tun = remote.connect({ relay: state.config.relay , config: state.config - , sortingHat: state.config.sortingHat || './lib/sorting-hat.js' - , locals: state.config.servernames - , services: state.services + , sortingHat: state.config.sortingHat , net: state.net , insecure: state.config.relay_ignore_invalid_certificates , token: state.token @@ -157,6 +113,7 @@ function connectTunnel() { , configDir: state.greenlock.configDir || '~/acme/etc/' // TODO, store: require(state.greenlock.store.name || 'le-store-certbot').create(state.greenlock.store.options || {}) , approveDomains: function (opts, certs, cb) { + console.log("trying approve domains"); // Certs being renewed are listed in certs.altnames if (certs) { opts.domains = certs.altnames; @@ -164,16 +121,22 @@ function connectTunnel() { return; } - if (-1 !== state.config.servernames.indexOf(opts.domains[0])) { + // by virtue of the fact that it's being tunneled through a + // trusted source that is already checking, we're good + //if (-1 !== state.config.servernames.indexOf(opts.domains[0])) { opts.email = state.greenlock.email || state.config.email; opts.agreeTos = state.greenlock.agree || state.agreeTos; cb(null, { options: opts, certs: certs }); return; - } + //} + + //cb(new Error("servername not found in allowed list")); } } }); + require(state.config.sortingHat).print(state.config); + function sigHandler() { console.log('SIGINT'); @@ -208,7 +171,7 @@ function rawTunnel() { if (!state.config.token) { var jwt = require('jsonwebtoken'); var tokenData = { - domains: state.config.servernames + domains: Object.keys(state.config.servernames).filter(function (name) { return /\./.test(name); }) , aud: aud , iss: Math.round(Date.now() / 1000) }; diff --git a/lib/sorting-hat.js b/lib/sorting-hat.js index 2d66019..04e3770 100644 --- a/lib/sorting-hat.js +++ b/lib/sorting-hat.js @@ -1,16 +1,185 @@ +module.exports.print = function (config) { + var services = { https: {}, http: {}, tcp: {} }; + // Note: the remote needs to know: + // what servernames to forward + // what ports to forward + // what udp ports to forward + // redirect http to https automatically + // redirect www to nowww automatically + if (config.http) { + Object.keys(config.http).forEach(function (hostname) { + if ('*' === hostname) { + config.servernames.forEach(function (servername) { + services.https[servername] = config.http[hostname]; + services.http[servername] = 'redirect-https'; + }); + return; + } + services.https[hostname] = config.http[hostname]; + services.http[hostname] = 'redirect-https'; + }); + } + /* + Object.keys(config.localPorts).forEach(function (port) { + var proto = config.localPorts[port]; + if (!proto) { return; } + if ('http' === proto) { + config.servernames.forEach(function (servername) { + services.http[servername] = port; + }); + return; + } + if ('https' === proto) { + config.servernames.forEach(function (servername) { + services.https[servername] = port; + }); + return; + } + if (true === proto) { proto = 'tcp'; } + if ('tcp' !== proto) { throw new Error("unsupported protocol '" + proto + "'"); } + //services[proxy.protocol]['*'] = proxy.port; + //services[proxy.protocol][proxy.hostname] = proxy.port; + services[proto]['*'] = port; + }); + */ + + Object.keys(services).forEach(function (protocol) { + var subServices = services[protocol]; + Object.keys(subServices).forEach(function (hostname) { + console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]); + }); + }); + console.info(''); +}; + module.exports.assign = function (state, tun, cb) { var net = state.net || require('net'); - var service = tun.service.toLowerCase(); - var portList = state.services[service]; - var port; if (!tun.name && !tun.serviceport) { console.log('tun:\n',tun); //console.warn(tun.data.toString()); - cb(new Error("missing routing information for ':tun_id'")); + cb(new Error("No routing information for ':tun_id'. Missing both 'name' and 'serviceport'.")); return; } + if (!state.config.servernames) { + state.config.servernames = {}; + } + + var handlers = {}; + handlers.http = function (socket) { + if (!state.greenlock) { + state.greenlock = require('greenlock').create(state.greenlockConfig); + } + if (!state.httpRedirectServer) { + state.redirectHttps = require('redirect-https')(); + state.httpRedirectServer = require('http').createServer(state.greenlock.middleware(state.redirectHttps)); + } + state.httpRedirectServer.emit('connection', socket); + }; + handlers.https = function (tlsSocket) { + if (!state.defaultHttpServer) { + state.defaultHttpServer = require('http').createServer(function (req, res) { + console.log('[hit http/s server]'); + res.end('Hello, Encrypted Tunnel World!'); + }); + } + state.defaultHttpServer.emit('connection', tlsSocket); + }; + + if ('http' === tun.service || 'https' === tun.service) { + if (!tun.name) { + cb(new Error("No routing information for ':tun_id'. Service '" + tun.service + "' is missing 'name'.")); + return; + } + } + + function redirectHttp(cb) { + var socketPair = require('socket-pair'); + conn = socketPair.create(function (err, other) { + if (err) { cb(err); return; } + handlers.http(other); + cb(null, conn); + }); + //if (tun.data) { conn.write(tun.data); } + return conn; + } + + var handled; + + if ('http' === tun.service) { + // TODO match *.example.com + handled = Object.keys(state.config.servernames).some(function (sn) { + if (sn !== tun.name) { return; } + + console.log('Found config match for PLAIN', tun.name); + if (!state.config.servernames[sn]) { return; } + + if (false === state.config.servernames[sn].terminate) { + cb(new Error("insecure http not supported yet")); + return true; + } + + console.log('Redirecting HTPTP for', tun.name); + redirectHttp(cb); + return true; + }); + if (!handled) { + redirectHttp(cb); + } + return; + } + + function terminateTls(cb) { + var socketPair = require('socket-pair'); + conn = socketPair.create(function (err, other) { + if (err) { cb(err); return; } + + if (!state.greenlock) { + state.greenlock = require('greenlock').create(state.greenlockConfig); + } + if (!state.terminatorServer) { + state.terminatorServer = require('tls').createServer(state.greenlock.tlsOptions, function (tlsSocket) { + console.log('[hit tls server]'); + if (err) { cb(err); return; } + handlers.https(tlsSocket); + }); + } + + console.log('[emitting tls connection]'); + state.terminatorServer.emit('connection', other); + cb(null, conn); + }); + //if (tun.data) { conn.write(tun.data); } + return conn; + } + + if ('https' === tun.service) { + // TODO match *.example.com + handled = Object.keys(state.config.servernames).some(function (sn) { + if (sn !== tun.name) { return; } + + console.log('Found config match for TLS', tun.name); + if (!state.config.servernames[sn]) { return; } + + if (false === state.config.servernames[sn].terminate) { + cb(new Error("insecure http not supported yet")); + return true; + } + + console.log('Terminating TLS for', tun.name); + terminateTls(cb); + return true; + }); + if (!handled) { + terminateTls(cb); + } + return; + } + + return; + var portList = state.services[service]; + var port; port = portList[tun.name]; if (!port) { // Check for any wildcard domains, sorted longest to shortest so the one with the @@ -48,7 +217,7 @@ module.exports.assign = function (state, tun, cb) { function handleNow(socket) { var httpServer; var tlsServer; - if ('https' === service) { + if ('https' === tun.service) { if (!state.greenlock) { state.greenlock = require('greenlock').create(state.greenlockConfig); } @@ -77,9 +246,7 @@ module.exports.assign = function (state, tun, cb) { conn = socketPair.create(function (err, other) { if (err) { console.error('[Error] ' + err.message); } handleNow(other); - if (createOpts.data) { - conn.write(createOpts.data); - } + //if (createOpts.data) { conn.write(createOpts.data); } }); /* var streamPair = require('stream-pair'); @@ -96,9 +263,7 @@ module.exports.assign = function (state, tun, cb) { // this will happen before 'data' or 'readable' is triggered // We use the data from the createOpts object so that the createConnection function has // the oppurtunity of removing/changing it if it wants/needs to handle it differently. - if (createOpts.data) { - conn.write(createOpts.data); - } + //if (createOpts.data) { conn.write(createOpts.data); } }); } cb(null, conn); diff --git a/package.json b/package.json index 6bcbf9e..b241153 100644 --- a/package.json +++ b/package.json @@ -52,11 +52,11 @@ "greenlock": "^2.2.19", "js-yaml": "^3.11.0", "jsonwebtoken": "^7.1.9", + "proxy-packer": "^1.4.3", "recase": "^1.0.4", "redirect-https": "^1.1.5", "sni": "^1.0.0", "socket-pair": "^1.0.3", - "proxy-packer": "^1.4.3", "ws": "^2.2.3" } } diff --git a/remote.js b/remote.js index a737097..676b6ad 100644 --- a/remote.js +++ b/remote.js @@ -281,6 +281,7 @@ function run(state) { return; } clientHandlers.add(conn, cid, tun); + if (tun.data) { conn.write(tun.data); } wstunneler.resume(); }); }