removed tunnel from config and API and made DDNS responsible
This commit is contained in:
		
							parent
							
								
									5cc7e3f187
								
							
						
					
					
						commit
						0dd20e4dfc
					
				
							
								
								
									
										18
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								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 | ||||
|  | ||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								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 | ||||
|  | ||||
| @ -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 <file>', '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 <email>', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.") | ||||
|   .option('--debug', "Enable debug output") | ||||
|   .parse(process.argv); | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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 | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user