From 1928278e3f17cac077b6b1cd4ca2c5cc1dc5e8f4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 29 Dec 2015 18:32:37 +0000 Subject: [PATCH] begin cli --- bin/holepunch.js | 94 ++++++++++++++++++++++++++++++++++++++++ lib/loopback-listener.js | 60 +++++++++++++++++++++++++ lib/middleware.js | 60 +++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 bin/holepunch.js create mode 100644 lib/loopback-listener.js create mode 100644 lib/middleware.js diff --git a/bin/holepunch.js b/bin/holepunch.js new file mode 100644 index 0000000..c16de1b --- /dev/null +++ b/bin/holepunch.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node +'use strict'; + +var cli = require('cli'); +//var mkdirp = require('mkdirp'); + +// TODO link with RVPN service: server, email, domains, agree-tos +// TODO txt records for browser plugin: TXT _http.example.com _https.example.com +cli.parse({ + debug: [ false, " show traces and logs", 'boolean', false ] +, 'plain-ports': [ false, " Port numbers to test with plaintext loopback. (default: 65080) (formats: ,,)", 'string' ] +, 'tls-ports': [ false, " Port numbers to test with tls loopback. (default: null)", 'string' ] +, 'ipify-urls': [ false, " Comma separated list of URLs to test for external ip. (default: api.ipify.org)", 'string' ] +, 'protocols': [ false, " Comma separated list of ip mapping protocols. (default: none,upnp,pmp)", 'string' ] +, 'rvpn-configs': [ false, " Comma separated list of Reverse VPN config files in the order they should be tried. (default: null)", 'string' ] +// TODO allow standalone, webroot, etc +}); + +// ignore certonly and extraneous arguments +cli.main(function(_, options) { + console.log(''); + var args = {}; + //var hp = require('../'); + + function parsePorts(portstr) { + var parts = portstr.split(':'); + var opts = { + internal: parseInt(parts[0], 10) + , external: (parts[1]||parts[0]).split('|').map(function (port) { + return parseInt(port, 10); + }) + }; + + return opts; + } + + function exists(x) { + return x; + } + + console.log('options'); + console.log(options); + args.debug = options.debug; + args.plainPorts = options['plain-ports']; + args.tlsPorts = options['tls-ports']; + args.protocols = options.protocols; + args.ipifyUrls = options['ipify-urls']; + args.rvpnConfigs = options['rvpn-configs']; + + if ('false' === args.ipifyUrls || false === args.ipifyUrls) { + args.ipifyUrls = []; + } else { + args.ipifyUrls = (args.ipifyUrls || 'api.ipify.org').split(','); + } + if ('false' === args.protocols || false === args.protocols) { + args.protocols = []; + } else { + args.protocols = (args.protocols || 'none,upnp,pmp').split(','); + } + // Coerce to string. cli returns a number although we request a string. + args.tlsPorts = (args.tlsPorts || "").toString().split(',').filter(exists).map(parsePorts); + args.rvpnConfigs = (args.rvpnConfigs || "").toString().split(',').filter(exists); + if ('false' === args.plainPorts || false === args.plainPorts) { + args.plainPorts = []; + } else { + args.plainPorts = (args.plainPorts || "65080").toString().split(',').map(parsePorts); + } + + console.log('args'); + console.log(args); + /* + return hp.create({ + debug: args.debug + , plainPorts: args.plainPorts + , tlsPorts: args.plainPorts + }).register(args, function (err, results) { + if (err) { + console.error('[Error]: letsencrypt-cli'); + console.error(err.stack || err); + return; + } + + // should get back account, path to certs, pems, etc? + console.log('\nCertificates installed at:'); + console.log(Object.keys(results).filter(function (key) { + return /Path/.test(key); + }).map(function (key) { + return results[key]; + }).join('\n')); + + process.exit(0); + }); + */ +}); diff --git a/lib/loopback-listener.js b/lib/loopback-listener.js new file mode 100644 index 0000000..667459e --- /dev/null +++ b/lib/loopback-listener.js @@ -0,0 +1,60 @@ +'use strict'; + +var http = require('http'); +var https = require('https'); +var express = require('express'); + +var middleware = module.exports.middleware = require('./middleware'); + +module.exports.create = function (opts) { + var httpsOptions = opts.httpsOptions || require('localhost.daplie.com-certificates'); + var results = { + plainServers: [] + , tlsServers: [] + }; + var app = express(); + + app.use('/', middleware(opts)); + + (opts.plainPorts||[]).forEach(function (plainPort) { + var plainServer = http.createServer(); + plainServer.__plainPort = plainPort; + http.on('request', app); + results.plainServers.push(plainServer); + }); + + (opts.tlsPorts||[]).forEach(function (tlsPort) { + var tlsServer = https.createServer(httpsOptions); + tlsServer.__tlsPort = tlsPort; + http.on('request', app); + results.tlsServers.push(tlsServer); + }); + + function onListen() { + /*jshint validthis: true*/ + var server = this; + var addr = server.address(); + var proto = 'honorCipherOrder' in server ? 'https' : 'http'; + + console.info('Listening on ' + proto + '://' + addr.address + ':' + addr.port); + } + + process.nextTick(function () { + results.plainServers.forEach(function (plainServer) { + plainServer.listen( + plainServer.__plainPort.port + , plainServer.__plainPort.address || '0.0.0.0' + , onListen + ); + }); + results.tlsServers.forEach(function (tlsServer) { + tlsServer.listen( + tlsServer.__tlsPort.port + , tlsServer.__tlsPort.address || '0.0.0.0' + , onListen + ); + }); + }); + + return results; +}; diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 0000000..573d006 --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,60 @@ +'use strict'; + +var scmp = require('scmp'); + +function middleware(opts) { + var key = opts.key; + var val = opts.value; + var vhost = opts.vhost; + var pathnamePrefix = opts.prefix || '/.well-known/com.daplie.loopback/'; + var defaultHostname = 'loopback.daplie.invalid'; + + if (!key) { + opts.key = require('crypto').randomBytes(8).toString('hex'); + } + if (!val) { + opts.value = require('crypto').randomBytes(16).toString('hex'); + } + if (!vhost && vhost !== false) { + vhost = defaultHostname; + } + if ('/' !== pathnamePrefix[pathnamePrefix.length - 1]) { + pathnamePrefix += '/'; + } + + return function (req, res, next) { + var hostname = (req.hostname || req.headers.host || '').toLowerCase(); + var urlpath = (req.pathname || req.url); + + if (vhost !== false && vhost !== hostname) { + if (opts.debug) { + console.log("[HP] Host '" + hostname + "' failed to match '" + vhost + "'"); + } + next(); + return; + } + + if (0 !== urlpath.indexOf(pathnamePrefix)) { + if (opts.debug) { + console.log("[HP] Pathname '" + urlpath + "'" + + " failed to match '" + pathnamePrefix + "'"); + } + next(); + return; + } + + if (scmp(key, urlpath.substr(pathnamePrefix.length, key.length))) { + if (opts.debug) { + console.log("[HP] Pathname '" + urlpath + "'" + + " failed to match '" + pathnamePrefix + key + "'"); + } + next(); + return; + } + + res.setHeader('Content-Type', 'text/plain'); + res.end(val); + }; +} + +module.exports = middleware;