diff --git a/API.md b/API.md index bfb6c54..6495f06 100644 --- a/API.md +++ b/API.md @@ -9,24 +9,6 @@ localhost.admin.daplie.me All requests require an OAuth3 token in the request headers. -## Tunnel - -### Check Status - * **URL** `/api/goldilocks@daplie.com/tunnel` - * **Method** `POST` - * **Reponse**: An object whose keys are the URLs for the tunnels, and whose - properties are arrays of the tunnel tokens. - - This route with return only the sessions started by the same user who is - checking the status. - -### Start Tunnel - * **URL** `/api/goldilocks@daplie.com/tunnel` - * **Method** `POST` - - This route will use the stored token for the user matching the request - header to request a tunnel token from the audience of the stored token. - ## Socks5 Proxy ### Check Status diff --git a/README.md b/README.md index dae6727..4491569 100644 --- a/README.md +++ b/README.md @@ -356,20 +356,6 @@ The tunnel client is meant to be run from behind a firewalls, carrier-grade NAT, or otherwise inaccessible devices to allow them to be accessed publicly on the internet. -It has no options per se, but is rather a list of tokens that can be used to -connect to tunnel servers. If the token does not have an `aud` field it must be -provided in an object with the token provided in the `jwt` field and the tunnel -server url provided in the `tunnelUrl` field. - -Example config: - -```yml -tunnel: - - 'some.jwt_encoded.token' - - jwt: 'other.jwt_encoded.token' - tunnelUrl: 'wss://api.tunnel.example.com/' -``` - ### ddns TODO diff --git a/bin/goldilocks.js b/bin/goldilocks.js index 51d77d2..4e651dc 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -212,8 +212,6 @@ function fillConfig(config, args) { config.addresses = addresses; config.device = { hostname: require('os').hostname() }; - config.tunnel = args.tunnel || config.tunnel; - if (Array.isArray(config.tcp.bind)) { return PromiseA.resolve(config); } @@ -310,8 +308,7 @@ function readEnv(args) { } catch (err) {} var env = { - tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true - , email: process.env.GOLDILOCKS_EMAIL + email: process.env.GOLDILOCKS_EMAIL , cwd: process.env.GOLDILOCKS_HOME || process.cwd() , debug: process.env.GOLDILOCKS_DEBUG && true }; @@ -325,7 +322,6 @@ program .version(require('../package.json').version) .option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)") .option('-c --config ', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json') - .option('--tunnel [token]', 'Turn tunnel on. This will enter interactive mode for login if no token is specified.') .option('--email ', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.") .option('--debug', "Enable debug output") .parse(process.argv); diff --git a/etc/goldilocks/goldilocks.example.yml b/etc/goldilocks/goldilocks.example.yml index 38ec6b5..86ea64a 100644 --- a/etc/goldilocks/goldilocks.example.yml +++ b/etc/goldilocks/goldilocks.example.yml @@ -9,11 +9,6 @@ tcp: - 22 address: '127.0.0.1:8022' -# tunnel: jwt -# tunnel: -# - jwt1 -# - jwt2 - tunnel_server: secret: abc123 servernames: @@ -91,3 +86,10 @@ mdns: port: 5353 broadcast: '224.0.0.251' ttl: 300 + +ddns: + enabled: true + domains: + - www.example.com + - api.example.com + - test.example.com diff --git a/lib/ddns/index.js b/lib/ddns/index.js index 5c71a78..e63f131 100644 --- a/lib/ddns/index.js +++ b/lib/ddns/index.js @@ -5,7 +5,8 @@ module.exports.create = function (deps, conf) { var loopback = require('./loopback').create(deps, conf); var dnsCtrl = require('./dns-ctrl').create(deps, conf); - var localAddr, gateway, accessible; + var localAddr, gateway; + var tunnelActive = false; async function checkNetworkEnv() { // Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck // what network environment we are in we check our local network address and the gateway to @@ -23,17 +24,25 @@ module.exports.create = function (deps, conf) { return !loopResult.ports[port]; }); - // All ports come back to us, so we are either a public address or the router has already - // been configured to forward these ports to us, so no configuration needs to be done we - // just have to make sure the DNS records stay in sync with our public address. - if (!notLooped.length) { - accessible = true; - return; - } + // if (notLooped.length) { + // // TODO: try to automatically configure router to forward ports to us. + // } - // TODO: try to automatically configure router to forward ports to us. - accessible = false; - // TODO: move tunnel client here as fall back. + // If we are on a public accress or all ports we are listening on are forwarded to us then + // we don't need the tunnel and we can set the DNS records for all our domains to our public + // address. Otherwise we need to use the tunnel to accept traffic. + if (!notLooped.length) { + if (tunnelActive) { + deps.tunnelClients.disconnect(); + tunnelActive = false; + } + } else { + if (!tunnelActive) { + var session = await getSession(); + await deps.tunnelClients.start(session, conf.dns.domains); + tunnelActive = true; + } + } } async function getSession() { @@ -61,7 +70,7 @@ module.exports.create = function (deps, conf) { } await checkNetworkEnv(); - if (!accessible) { + if (tunnelActive) { return; } var session = await getSession(); diff --git a/lib/tunnel-client-manager.js b/lib/tunnel-client-manager.js index 021e04f..5355295 100644 --- a/lib/tunnel-client-manager.js +++ b/lib/tunnel-client-manager.js @@ -1,109 +1,15 @@ 'use strict'; module.exports.create = function (deps, config) { - var PromiseA = require('bluebird'); - var fs = PromiseA.promisifyAll(require('fs')); var stunnel = require('stunnel'); var activeTunnels = {}; - var path = require('path'); - var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json'); - var storage = { - _read: function () { - var tokens; - try { - tokens = require(tokensPath); - } catch (err) { - tokens = {}; - } - return tokens; - } - , _write: function (tokens) { - return fs.mkdirAsync(path.dirname(tokensPath)).catch(function (err) { - if (err.code !== 'EEXIST') { - console.error('failed to mkdir', path.dirname(tokensPath), err.toString()); - } - }).then(function () { - return fs.writeFileAsync(tokensPath, JSON.stringify(tokens), 'utf8'); - }); - } - , _makeKey: function (token) { - // We use a stripped down version of the token contents so that if the token is - // re-issued the nonce and the iat and any other less important things are different - // we don't save essentially duplicate tokens multiple times. - var parsed = JSON.parse((new Buffer(token.split('.')[1], 'base64')).toString()); - var stripped = {}; - ['aud', 'iss', 'domains'].forEach(function (key) { - if (parsed[key]) { - stripped[key] = parsed[key]; - } - }); - stripped.domains.sort(); - - var hash = require('crypto').createHash('sha256'); - return hash.update(JSON.stringify(stripped)).digest('hex'); - } - - , all: function () { - var tokens = storage._read(); - return PromiseA.resolve(Object.keys(tokens).map(function (key) { - return tokens[key]; - })); - } - , save: function (token) { - return PromiseA.resolve().then(function () { - var curTokens = storage._read(); - curTokens[storage._makeKey(token.jwt)] = token; - return storage._write(curTokens); - }); - } - , del: function (token) { - return PromiseA.resolve().then(function () { - var curTokens = storage._read(); - delete curTokens[storage._makeKey(token.jwt)]; - return storage._write(curTokens); - }); - } - }; - - function acquireToken(session) { - var OAUTH3 = deps.OAUTH3; - // session seems to be changed by the API call for some reason, so save the - // owner before that happens. - var owner = session.id; - - // The OAUTH3 library stores some things on the root session object that we usually - // just leave inside the token, but we need to pull those out before we use it here - session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss; - session.client_uri = session.client_uri || session.token.azp; - session.scope = session.scope || session.token.scp; - - console.log('asking for tunnel token from', session.token.aud); - return OAUTH3.discover(session.token.aud).then(function (directives) { - var opts = { - api: 'tunnel.token' - , session: session - , data: { - // filter to all domains that are on this device - //domains: Object.keys(domainsMap) - device: { - hostname: config.device.hostname - , id: config.device.uid || config.device.id - } - } - }; - - return OAUTH3.api(directives.api, opts).then(function (result) { - console.log('got a token from the tunnel server?'); - result.owner = owner; - return result; - }); - }); - } - function addToken(data) { + if (typeof data === 'string') { + data = { jwt: data }; + } if (!data.jwt) { - return PromiseA.reject(new Error("missing 'jwt' from tunnel data")); + return deps.PromiseA.reject(new Error("missing 'jwt' from tunnel data")); } if (!data.tunnelUrl) { var decoded; @@ -111,12 +17,12 @@ module.exports.create = function (deps, config) { decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii')); } catch (err) { console.warn('invalid web token given to tunnel manager', err); - return PromiseA.reject(err); + return deps.PromiseA.reject(err); } if (!decoded.aud) { console.warn('tunnel manager given token with no tunnelUrl or audience'); var err = new Error('missing tunnelUrl and audience'); - return PromiseA.reject(err); + return deps.PromiseA.reject(err); } data.tunnelUrl = 'wss://' + decoded.aud + '/'; } @@ -146,19 +52,49 @@ module.exports.create = function (deps, config) { return activeTunnels[data.tunnelUrl].append(data.jwt); } + function acquireToken(session, domains) { + var OAUTH3 = deps.OAUTH3; + + // The OAUTH3 library stores some things on the root session object that we usually + // just leave inside the token, but we need to pull those out before we use it here + session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss; + session.client_uri = session.client_uri || session.token.azp; + session.scope = session.scope || session.token.scp; + + console.log('asking for tunnel token from', session.token.aud); + return OAUTH3.discover(session.token.aud).then(function (directives) { + var opts = { + api: 'tunnel.token' + , session: session + , data: { + domains: domains + , device: { + hostname: config.device.hostname + , id: config.device.uid || config.device.id + } + } + }; + + return OAUTH3.api(directives.api, opts).then(addToken); + }); + } + function removeToken(data) { + if (typeof data === 'string') { + data = { jwt: data }; + } if (!data.tunnelUrl) { var decoded; try { decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii')); } catch (err) { console.warn('invalid web token given to tunnel manager', err); - return PromiseA.reject(err); + return deps.PromiseA.reject(err); } if (!decoded.aud) { console.warn('tunnel manager given token with no tunnelUrl or audience'); var err = new Error('missing tunnelUrl and audience'); - return PromiseA.reject(err); + return deps.PromiseA.reject(err); } data.tunnelUrl = 'wss://' + decoded.aud + '/'; } @@ -166,72 +102,23 @@ module.exports.create = function (deps, config) { // Not sure if we actually want to return an error that the token didn't even belong to a // server that existed, but since it never existed we can consider it as "removed". if (!activeTunnels[data.tunnelUrl]) { - return PromiseA.resolve(); + return deps.PromiseA.resolve(); } console.log('removing token from tunnel at', data.tunnelUrl); return activeTunnels[data.tunnelUrl].clear(data.jwt); } - if (config.tunnel) { - var confTokens = config.tunnel; - if (typeof confTokens === 'string') { - confTokens = confTokens.split(','); - } - confTokens.forEach(function (jwt) { - if (typeof jwt === 'object') { - jwt.owner = 'config'; - addToken(jwt); - } else { - addToken({ jwt: jwt, owner: 'config' }); - } + function disconnectAll() { + Object.keys(activeTunnels).forEach(function (key) { + activeTunnels[key].end(); }); } - - storage.all().then(function (stored) { - stored.forEach(function (result) { - addToken(result); - }); - }); - return { - start: function (session) { - return acquireToken(session).then(function (token) { - return addToken(token).then(function () { - return storage.save(token); - }); - }); - } - , add: function (data) { - return addToken(data).then(function () { - return storage.save(data); - }); - } - , remove: function (data) { - return storage.del(data.jwt).then(function () { - return removeToken(data); - }); - } - , get: function (owner) { - return storage.all().then(function (tokens) { - var result = {}; - tokens.forEach(function (data) { - if (!result[data.owner]) { - result[data.owner] = {}; - } - if (!result[data.owner][data.tunnelUrl]) { - result[data.owner][data.tunnelUrl] = []; - } - data.decoded = JSON.parse(new Buffer(data.jwt.split('.')[0], 'base64')); - result[data.owner][data.tunnelUrl].push(data); - }); - - if (owner) { - return result[owner] || {}; - } - return result; - }); - } + start: acquireToken + , startDirect: addToken + , remove: removeToken + , disconnect: disconnectAll }; }; diff --git a/packages/apis/com.daplie.goldilocks/index.js b/packages/apis/com.daplie.goldilocks/index.js index 8dfbd00..fb7f0fc 100644 --- a/packages/apis/com.daplie.goldilocks/index.js +++ b/packages/apis/com.daplie.goldilocks/index.js @@ -235,33 +235,6 @@ module.exports.create = function (deps, conf) { }); } - , tunnel: function (req, res) { - if (handleCors(req, res)) { - return; - } - isAuthorized(req, res, function () { - if ('POST' !== req.method) { - res.setHeader('Content-Type', 'application/json'); - return deps.tunnelClients.get(req.userId).then(function (result) { - res.end(JSON.stringify(result)); - }, function (err) { - res.statusCode = 500; - res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } })); - }); - } - - return deps.storage.owners.get(req.userId).then(function (session) { - return deps.tunnelClients.start(session).then(function () { - res.setHeader('Content-Type', 'application/json;'); - res.end(JSON.stringify({ success: true })); - }, function (err) { - res.setHeader('Content-Type', 'application/json;'); - res.statusCode = 500; - res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } })); - }); - }); - }); - } , config: function (req, res) { if (handleCors(req, res)) { return;