diff --git a/bin/goldilocks.js b/bin/goldilocks.js index 6d7a9ed..59312c7 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -114,7 +114,7 @@ function readConfigAndRun(args) { // TODO maybe move to config.state.addresses (?) config.addresses = addresses; - config.device = { hostname: 'TODO: fetch hostname from device and from ip and try to make a match' }; + config.device = { hostname: 'daplien-pod' }; if (config.tcp.ports) { run(config); diff --git a/lib/app.js b/lib/app.js index 9e128b6..caf4289 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function (deps, conf, overrideHttp) { +module.exports = function (myDeps, conf, overrideHttp) { var express = require('express'); //var finalhandler = require('finalhandler'); var serveStatic = require('serve-static'); @@ -100,118 +100,132 @@ module.exports = function (deps, conf, overrideHttp) { } }; - return require('../packages/apis/com.daplie.goldilocks').create({ - PromiseA: PromiseA - , OAUTH3: OAUTH3 - , storage: { - owners: owners + myDeps.PromiseA = PromiseA; + myDeps.OAUTH3 = OAUTH3; + myDeps.storage = { owners: owners }; + myDeps.recase = require('recase').create({}); + myDeps.request = request; + myDeps.api = { + // TODO move loopback to oauth3.api('tunnel:loopback') + loopback: function (deps, session, opts2) { + var crypto = require('crypto'); + var token = crypto.randomBytes(16).toString('hex'); + var keyAuthorization = crypto.randomBytes(16).toString('hex'); + var nonce = crypto.randomBytes(16).toString('hex'); + + // TODO set token and keyAuthorization to /.well-known/cloud-challenge/:token + return request({ + method: 'POST' + , url: 'https://oauth3.org/api/org.oauth3.tunnel/loopback' + , json: { + address: opts2.address + , port: opts2.port + , token: token + , keyAuthorization: keyAuthorization + , servername: opts2.servername + , nonce: nonce + , scheme: 'https' + , iat: Date.now() + } + }).then(function (result) { + // TODO this will always fail at the moment + console.log('loopback result:'); + return result; + }); } - , recase: require('recase').create({}) - , request: request - , options: conf - , api: { - // TODO move loopback to oauth3.api('tunnel:loopback') - loopback: function (deps, session, opts2) { - var crypto = require('crypto'); - var token = crypto.randomBytes(16).toString('hex'); - var keyAuthorization = crypto.randomBytes(16).toString('hex'); - var nonce = crypto.randomBytes(16).toString('hex'); - - // TODO set token and keyAuthorization to /.well-known/cloud-challenge/:token - return request({ - method: 'POST' - , url: 'https://oauth3.org/api/org.oauth3.tunnel/loopback' - , json: { - address: opts2.address - , port: opts2.port - , token: token - , keyAuthorization: keyAuthorization - , servername: opts2.servername - , nonce: nonce - , scheme: 'https' - , iat: Date.now() - } - }).then(function (result) { - // TODO this will always fail at the moment - console.log('loopback result:'); - return result; - }); - } - , tunnel: function (deps, session) { - // TODO save session to config and turn tunnel on - var OAUTH3 = deps.OAUTH3; - var url = require('url'); - var providerUri = session.token.aud; - var urlObj = url.parse(OAUTH3.url.normalize(session.token.azp)); - var oauth3 = OAUTH3.create(urlObj, { - providerUri: providerUri - , session: session - }); - //var crypto = require('crypto'); - //var id = crypto.createHash('sha256').update(session.token.sub).digest('hex'); - return oauth3.setProvider(providerUri).then(function () { - /* - return oauth3.api('domains.list').then(function (domains) { - var domainsMap = {}; - domains.forEach(function (d) { - if (!d.device) { - return; - } - if (d.device !== deps.options.device.hostname) { - return; - } - domainsMap[d.name] = true; - }); - */ - - //console.log('domains matching hostname', Object.keys(domainsMap)); - //console.log('device', deps.options.device); - return oauth3.api('tunnel.token', { - data: { - // filter to all domains that are on this device - //domains: Object.keys(domainsMap) - device: { - hostname: deps.options.device.hostname - , id: deps.options.device.uid || deps.options.device.id - } - } - }).then(function (result) { - console.log('got a token from the tunnel server?'); - console.log(result); - if (!result.tunnelUrl) { - result.tunnelUrl = ('wss://' + (new Buffer(result.jwt.split('.')[1], 'base64').toString('ascii')).aud + '/'); - } - var opts3 = { - token: result.jwt - , stunneld: result.tunnelUrl - // we'll provide faux networking and pipe as we please - , services: { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ } - , net: deps.tunnel.net || deps.net - }; - - if (tun) { - if (tun.append) { - tun.append(result.jwt); - } - else if (tun.end) { - tun.end(); - tun = null; - } - } - - if (!tun) { - tun = stunnel.connect(opts3); - conf.tun = true; - } - }); - /* + , tunnel: function (deps, session) { + // TODO save session to config and turn tunnel on + var OAUTH3 = deps.OAUTH3; + var url = require('url'); + var providerUri = session.token.aud; + var urlObj = url.parse(OAUTH3.url.normalize(session.token.azp)); + var oauth3 = OAUTH3.create(urlObj, { + providerUri: providerUri + , session: session + }); + //var crypto = require('crypto'); + //var id = crypto.createHash('sha256').update(session.token.sub).digest('hex'); + return oauth3.setProvider(providerUri).then(function () { + /* + return oauth3.api('domains.list').then(function (domains) { + var domainsMap = {}; + domains.forEach(function (d) { + if (!d.device) { + return; + } + if (d.device !== conf.device.hostname) { + return; + } + domainsMap[d.name] = true; }); - */ + */ + + //console.log('domains matching hostname', Object.keys(domainsMap)); + //console.log('device', conf.device); + return oauth3.api('tunnel.token', { + data: { + // filter to all domains that are on this device + //domains: Object.keys(domainsMap) + device: { + hostname: conf.device.hostname + , id: conf.device.uid || conf.device.id + } + } + }).then(function (result) { + console.log('got a token from the tunnel server?'); + console.log(result); + if (!result.tunnelUrl) { + result.tunnelUrl = ('wss://' + (new Buffer(result.jwt.split('.')[1], 'base64').toString('ascii')).aud + '/'); + } + var services = { https: { '*': 443 }, http: { '*': 80 }, smtp: { '*': 25}, smtps: { '*': 587 /*also 465/starttls*/ } /*, ssh: { '*': 22 }*/ }; + /* + console.log('blah'); + console.log(result.jwt); + console.log(result.tunnelUrl); + console.log(services); + console.log('deps.tunnel'); + console.log(deps.tunnel); + console.log('deps.tunnel.net'); + console.log(deps.tunnel.net.toString()); + console.log('deps.net'); + console.log(deps.net); + */ + var opts3 = { + token: result.jwt + , stunneld: result.tunnelUrl + // we'll provide faux networking and pipe as we please + , services: services + , net: myDeps.tunnel.net + }; + + console.log('blah 2'); + if (tun) { + console.log('balh 3'); + if (tun.append) { + tun.append(result.jwt); + } + else if (tun.end) { + tun.end(); + tun = null; + } + } + + console.log('might have tunnel?'); + if (!tun) { + console.log('connecting to the tunnel'); + tun = stunnel.connect(opts3); + conf.tun = true; + } + }); + /* }); - //, { token: token, refresh: refresh }); - } + */ + }); + //, { token: token, refresh: refresh }); } - }, conf); + }; + + return require('../packages/apis/com.daplie.goldilocks').create(myDeps, conf); } app = express(); diff --git a/lib/goldilocks.js b/lib/goldilocks.js index 73518cf..84efc70 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -11,8 +11,10 @@ module.exports.create = function (deps, config) { var modules = { }; var program = { tlsOptions: require('localhost.daplie.me-certificates').merge({}) - , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory' - , challengeType: 'tls-sni-01' +// , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory' + , acmeDirectoryUrl: 'https://acme-staging.api.letsencrypt.org/directory' +// , challengeType: 'tls-sni-01' // won't work with a tunnel + , challengeType: 'http-01' }; var secureContexts = {}; var tunnelAdminTlsOpts = {}; @@ -191,8 +193,8 @@ module.exports.create = function (deps, config) { function peek(conn, firstChunk, opts) { // TODO port/service-based routing can do here - // TLS - if (22 === firstChunk[0]) { + // TLS byte 1 is handshake and byte 6 is client hello + if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) { console.log('tryTls'); opts.servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid'; tlsRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, firstChunk, opts); @@ -205,6 +207,7 @@ module.exports.create = function (deps, config) { // even though we've already peeked, this logic is just as well to let be // since it works properly either way, unlike the tls socket conn.once('data', function (chunk) { + console.log('hyperPeek re-peek data', chunk.toString('utf8')); tcpRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, chunk, opts); }); return; @@ -275,7 +278,7 @@ module.exports.create = function (deps, config) { modules.http = require('./modules/http.js').create(deps, config); } modules.http.checkServername(opts.domain).then(function (stuff) { - if (!stuff.domains) { + if (!stuff || !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; @@ -284,9 +287,10 @@ module.exports.create = function (deps, config) { complete(null, { domain: stuff.domain || stuff.domains[0] , domains: stuff.domains - , email: program.email - , server: program.acmeDirectoryUrl - , challengeType: program.challengeType + , email: stuff.email || program.email + , server: stuff.acmeDirectoryUrl || program.acmeDirectoryUrl + , challengeType: stuff.challengeType || program.challengeType + , challenge: stuff.challenge }); return; }, cb); @@ -321,35 +325,76 @@ module.exports.create = function (deps, config) { deps.tunnel = deps.tunnel || {}; deps.tunnel.net = { createConnection: function (opts, cb) { - var Duplex = require('stream').Duplex; - var myDuplex = new Duplex(); + console.log('[gl.tunnel] creating connection'); + + // here "reader" means the socket that looks like the connection being accepted + // here "writer" means the remote-looking part of the socket that driving the connection + var writer; var wrapOpts = {}; + var rawTls = opts.tls || (0x16 === opts.data[0]) && (0x01 === opts.data[5]); - // this has the normal net/tcp stuff plus our custom stuff - // opts = { address, port, - // hostname, servername, encrypted, data, localAddress, localPort, remoteAddress, remotePort, remoteFamily } - Object.keys(opts).forEach(function (key) { - wrapOpts[key] = opts[key]; - myDuplex[key] = opts[key]; - }); + function usePair(err, reader) { + if (err) { + process.nextTick(function () { + writer.emit('error', err); + }); + return; + } + + // this has the normal net/tcp stuff plus our custom stuff + // opts = { address, port, + // hostname, servername, tls, encrypted, data, localAddress, localPort, remoteAddress, remotePort, remoteFamily } + Object.keys(opts).forEach(function (key) { + wrapOpts[key] = opts[key]; + try { + reader[key] = opts[key]; + } catch(e) { + // can't set real socket getters, like remoteAddr + } + }); + + // A few more extra specialty options + wrapOpts.localAddress = wrapOpts.localAddress || '127.0.0.2'; // TODO use the tunnel's external address + wrapOpts.localPort = wrapOpts.localPort || 'tunnel-0'; + try { + reader._remoteAddress = wrapOpts.remoteAddress; + reader._remotePort = wrapOpts.remotePort; + reader._localAddress = wrapOpts.localAddress; + reader._localPort = wrapOpts.localPort; + } catch(e) { + } + + netHandler(reader, wrapOpts); + + process.nextTick(function () { + //opts.data = wrapOpts.data; + + // this cb will cause the stream to emit its (actually) first data event + // (even though it already gave a peek into that first data chunk) + console.log('[tunnel] callback, data should begin to flow'); + cb(); + }); + } - // A few more extra specialty options wrapOpts.firstChunk = opts.data; wrapOpts.hyperPeek = !!opts.data; - wrapOpts.localAddress = wrapOpts.localAddress || '127.0.0.2'; // TODO use the tunnel's external address - wrapOpts.localPort = wrapOpts.localPort || 'tunnel-0'; + // encrypted meaning is *terminated* TLS + // tls meaning is *raw* TLS + if (rawTls) { + // TLS sockets must actually use a socket with a file descriptor + // https://nodejs.org/api/net.html#net_class_net_socket - netHandler(myDuplex, wrapOpts); + writer = require('socket-pair').create(function (err, other) { + usePair(err, other); + }); + } + else { + // stream-pair can only be used by TCP sockets, not tls + writer = require('stream-pair').create(); + usePair(null, writer.other); + } - process.nextTick(function () { - //opts.data = wrapOpts.data; - - // this cb will cause the stream to emit its (actually) first data event - // (even though it already gave a peek into that first data chunk) - cb(); - }); - - return myDuplex; + return writer; } }; diff --git a/package.json b/package.json index ccaa30a..b26bcd1 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "serve-index": "^1.7.0", "serve-static": "^1.10.0", "server-destroy": "^1.0.1", + "stream-pair": "^1.0.3", "stunnel": "git+https://git.daplie.com/Daplie/node-tunnel-client.git#v1" } }