Merge branch 'commercial' of https://git.ppl.family/ppl/commercial.telebit-relay.js into commercial
This commit is contained in:
		
						commit
						de9aab8195
					
				| @ -1,21 +1,24 @@ | ||||
| email: 'jon@example.com'       # must be valid (for certificate recovery and security alerts) | ||||
| agree_tos: true                # agree to the Telebit, Greenlock, and Let's Encrypt TOSes | ||||
| community_member: true         # receive infrequent relevant updates | ||||
| telemetry: true                # contribute to project telemetric data | ||||
| email: coolaj86@gmail.com | ||||
| agree_tos: true | ||||
| community_member: true | ||||
| telemetry: true | ||||
| webmin_domain: example.com | ||||
| shared_domain: xm.pl | ||||
| servernames:                   # hostnames that direct to the Telebit Relay admin console | ||||
|   - telebit.example.com | ||||
|   - telebit.example.net | ||||
| vhost: /srv/www/:hostname      # load secure websites at this path (uses template string, i.e. /var/www/:hostname/public) | ||||
| api_domain: example.com | ||||
| shared_domain: example.com | ||||
| servernames: | ||||
|   - www.example.com | ||||
|   - example.com | ||||
|   - api.example.com | ||||
| vhost: /srv/www/:hostname | ||||
| greenlock: | ||||
|   version: 'draft-11' | ||||
|   server: 'https://acme-v02.api.letsencrypt.org/directory' | ||||
|   store: | ||||
|     strategy: le-store-certbot # certificate storage plugin | ||||
|   config_dir: /etc/acme        # directory for ssl certificates | ||||
| secret: ''                     # generate with node -e "console.log(crypto.randomBytes(16).toString('hex'))" | ||||
|     strategy: le-store-certbot | ||||
|   config_dir: /opt/telebit-relay/etc/acme | ||||
| secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||
| mailer: | ||||
|   url: 'https://api.mailgun.net/v3/EXAMPLE.COM/messages' | ||||
|   api_key: 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | ||||
|   from: 'Example Mailer <MALIER@EXAMPLE.COM>' | ||||
| debug: true | ||||
|  | ||||
							
								
								
									
										5
									
								
								examples/telebitd.real.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								examples/telebitd.real.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| servernames: [ 'telebit.cloud' ] | ||||
| email: 'coolaj86@gmail.com' | ||||
| agree_tos: true | ||||
| community_member: false | ||||
| vhost: /srv/www/:hostname | ||||
| @ -179,6 +179,9 @@ if [ ! -f "$TELEBIT_RELAY_PATH/etc/$my_app.yml" ]; then | ||||
|   sudo bash -c "echo 'email: $my_email' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
|   sudo bash -c "echo 'secret: $my_secret' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
|   sudo bash -c "echo 'servernames: [ $my_servername ]' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
|   sudo bash -c "echo 'webmin_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
|   sudo bash -c "echo 'api_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
|   sudo bash -c "echo 'shared_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
|   sudo bash -c "cat $TELEBIT_RELAY_PATH/examples/$my_app.yml.tpl >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||
| fi | ||||
| 
 | ||||
|  | ||||
| @ -2,9 +2,24 @@ | ||||
| 
 | ||||
| var Devices = module.exports; | ||||
| Devices.add = function (store, servername, newDevice) { | ||||
|   var devices = store[servername] || []; | ||||
|   if (!store[servername]) { | ||||
|     store[servername] = []; | ||||
|   } | ||||
|   var devices = store[servername]; | ||||
|   devices.push(newDevice); | ||||
|   store[servername] = devices; | ||||
| }; | ||||
| Devices.alias = function (store, servername, alias) { | ||||
|   if (!store[servername]) { | ||||
|     store[servername] = []; | ||||
|   } | ||||
|   if (!store[servername]._primary) { | ||||
|     store[servername]._primary = servername; | ||||
|   } | ||||
|   if (!store[servername].aliases) { | ||||
|     store[servername].aliases = {}; | ||||
|   } | ||||
|   store[alias] = store[servername]; | ||||
|   store[servername].aliases[alias] = true; | ||||
| }; | ||||
| Devices.remove = function (store, servername, device) { | ||||
|   var devices = store[servername] || []; | ||||
| @ -17,9 +32,11 @@ Devices.remove = function (store, servername, device) { | ||||
|   return devices.splice(index, 1)[0]; | ||||
| }; | ||||
| Devices.list = function (store, servername) { | ||||
|   // efficient lookup first
 | ||||
|   if (store[servername] && store[servername].length) { | ||||
|     return store[servername]; | ||||
|     return store[servername]._primary && store[store[servername]._primary] || store[servername]; | ||||
|   } | ||||
| 
 | ||||
|   // There wasn't an exact match so check any of the wildcard domains, sorted longest
 | ||||
|   // first so the one with the biggest natural match with be found first.
 | ||||
|   var deviceList = []; | ||||
| @ -28,10 +45,19 @@ Devices.list = function (store, servername) { | ||||
|   }).sort(function (a, b) { | ||||
|     return b.length - a.length; | ||||
|   }).some(function (pattern) { | ||||
|     // '.example.com' = '*.example.com'.split(1)
 | ||||
|     var subPiece = pattern.slice(1); | ||||
|     // '.com' = 'sub.example.com'.slice(-4)
 | ||||
|     // '.example.com' = 'sub.example.com'.slice(-12)
 | ||||
|     if (subPiece === servername.slice(-subPiece.length)) { | ||||
|       console.log('"'+servername+'" matches "'+pattern+'"'); | ||||
|       console.log('[Devices.list] "'+servername+'" matches "'+pattern+'"'); | ||||
|       deviceList = store[pattern]; | ||||
| 
 | ||||
|       // Devices.alias(store, '*.example.com', 'sub.example.com'
 | ||||
|       // '*.example.com' retrieves a reference to 'example.com'
 | ||||
|       // and this reference then also referenced by 'sub.example.com'
 | ||||
|       // Hence this O(n) check is replaced with the O(1) check above
 | ||||
|       Devices.alias(store, pattern, servername); | ||||
|       return true; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
							
								
								
									
										1
									
								
								lib/extensions/admin/.well-known
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								lib/extensions/admin/.well-known
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| _apis | ||||
							
								
								
									
										1
									
								
								lib/extensions/admin/_apis/oauth3
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								lib/extensions/admin/_apis/oauth3
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| oauth3.org | ||||
							
								
								
									
										1
									
								
								lib/extensions/admin/_apis/oauth3.org
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								lib/extensions/admin/_apis/oauth3.org
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | ||||
| ../assets/oauth3.org/_apis/oauth3.org | ||||
							
								
								
									
										90
									
								
								lib/extensions/admin/account.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								lib/extensions/admin/account.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Telebit Account</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <form class="js-auth-form"> | ||||
|       <input class="js-auth-subject" type="email"/> | ||||
|       <button class="js-auth-submit" type="submit">Login</button> | ||||
|     </form> | ||||
| 
 | ||||
|     <script src="assets/oauth3.org/oauth3.core.js"></script> | ||||
|     <script> | ||||
|       (function () { | ||||
|         'use strict'; | ||||
|         var OAUTH3 = window.OAUTH3; | ||||
|         var oauth3 = OAUTH3.create({ | ||||
|           host: window.location.host | ||||
|         , pathname: window.location.pathname.replace(/\/[^\/]*$/, '/') | ||||
|         }); | ||||
|         var $ = function () { return document.querySelector.apply(document, arguments); } | ||||
| 
 | ||||
| 				function onChangeProvider(providerUri) { | ||||
| 					// example https://oauth3.org | ||||
| 					return oauth3.setIdentityProvider(providerUri); | ||||
| 				} | ||||
| 
 | ||||
| 				// This opens up the login window for the specified provider | ||||
| 				// | ||||
| 				function onClickLogin(ev) { | ||||
|           ev.preventDefault(); | ||||
|           ev.stopPropagation(); | ||||
| 
 | ||||
|           // TODO check subject for provider viability | ||||
|           return oauth3.authenticate({ | ||||
|             subject: $('.js-auth-subject').value | ||||
|           }).then(function (session) { | ||||
| 
 | ||||
| 						console.info('Authentication was Successful:'); | ||||
| 						console.log(session); | ||||
| 
 | ||||
| 						// You can use the PPID (or preferably a hash of it) as the login for your app | ||||
| 						// (it securely functions as both username and password which is known only by your app) | ||||
| 						// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key | ||||
| 						// | ||||
| 						console.info('Secure PPID (aka subject):', session.token.sub); | ||||
| 
 | ||||
| 						return oauth3.request({ | ||||
| 							url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json' | ||||
|                 .replace(/:sub/g, session.token.sub) | ||||
|                 .replace(/:kid/g, session.token.iss) | ||||
| 						, session: session | ||||
| 						}).then(function (resp) { | ||||
|               console.info("Public Key:"); | ||||
|               console.log(resp.data); | ||||
| 
 | ||||
|               return oauth3.request({ | ||||
|                 url: 'https://api.oauth3.org/api/issuer@oauth3.org/acl/profile' | ||||
|               , session: session | ||||
|               }).then(function (resp) { | ||||
| 
 | ||||
|                 console.info("Inspect Token:"); | ||||
|                 console.log(resp.data); | ||||
| 
 | ||||
|                 return oauth3.request({ | ||||
|                   url: 'https://api.telebit.cloud/api/telebit.cloud/account' | ||||
|                 , session: session | ||||
|                 }).then(function (resp) { | ||||
| 
 | ||||
|                   console.info("Telebit Account:"); | ||||
|                   console.log(resp.data); | ||||
| 
 | ||||
|                 }); | ||||
| 
 | ||||
| 
 | ||||
|               }); | ||||
| 
 | ||||
| 						}); | ||||
| 
 | ||||
| 					}, function (err) { | ||||
| 						console.error('Authentication Failed:'); | ||||
| 						console.log(err); | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
|         $('body form.js-auth-form').addEventListener('submit', onClickLogin); | ||||
|         onChangeProvider('oauth3.org'); | ||||
|       }()); | ||||
|     </script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										1
									
								
								lib/extensions/admin/assets/oauth3.org
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										1
									
								
								lib/extensions/admin/assets/oauth3.org
									
									
									
									
									
										Submodule
									
								
							| @ -0,0 +1 @@ | ||||
| Subproject commit 8e2e09f5823ae919c615c9c3b21114e01096b1ee | ||||
| @ -29,6 +29,9 @@ | ||||
|       <p>Friends enable friends to share anything, access anywhere, connect anytime.</p> | ||||
|     </center> | ||||
| 
 | ||||
|     <a href="account.html#/login">Login</a> | ||||
|     <a href="account.html#/create_account">Create Account</a> | ||||
| 
 | ||||
|     <div style="width: 800px; margin: auto;"> | ||||
|       <div> | ||||
|         <h2>Share and Test over HTTPS</h2> | ||||
|  | ||||
| @ -19,7 +19,7 @@ function checkStatus() { | ||||
|       } | ||||
|       if ('complete' === data.status) { | ||||
|         setTimeout(function () { | ||||
|           window.document.body.innerHTML += ('<img src="https://' + domainname + '/_apis/telebit.cloud/clear.gif">'); | ||||
|           //window.document.body.innerHTML += ('<img src="https://' + domainname + '/_apis/telebit.cloud/clear.gif">');
 | ||||
|           // TODO once this is loaded (even error) Let's Encrypt is done,
 | ||||
|           // then it's time to redirect to the domain. Yay!
 | ||||
|         }, 1 * 1000); | ||||
|  | ||||
| @ -6,7 +6,16 @@ var util = require('util'); | ||||
| var crypto = require('crypto'); | ||||
| var escapeHtml = require('escape-html'); | ||||
| var jwt = require('jsonwebtoken'); | ||||
| var requestAsync = util.promisify(require('request')); | ||||
| var requestAsync = util.promisify(require('@coolaj86/urequest')); | ||||
| var readFileAsync = util.promisify(fs.readFile); | ||||
| var mkdirpAsync = util.promisify(require('mkdirp')); | ||||
| 
 | ||||
| var PromiseA; | ||||
| try { | ||||
|   PromiseA = require('bluebird'); | ||||
| } catch(e) { | ||||
|   PromiseA = global.Promise; | ||||
| } | ||||
| 
 | ||||
| var _auths = module.exports._auths = {}; | ||||
| var Auths = {}; | ||||
| @ -77,6 +86,72 @@ Auths._clean = function () { | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| var sfs = require('safe-replace'); | ||||
| var Accounts = {}; | ||||
| Accounts._getTokenId = function (auth) { | ||||
|   return auth.data.sub + '@' + (auth.data.iss||'').replace(/\/|\\/g, '-'); | ||||
| }; | ||||
| Accounts._accPath = function (req, accId) { | ||||
|   return path.join(req._state.config.accountsDir, 'self', accId); | ||||
| }; | ||||
| Accounts._subPath = function (req, id) { | ||||
|   return path.join(req._state.config.accountsDir, 'oauth3', id); | ||||
| }; | ||||
| Accounts._setSub = function (req, id, subData) { | ||||
|   var subpath = Accounts._subPath(req, id); | ||||
|   return mkdirpAsync(subpath).then(function () { | ||||
|     return sfs.writeFileAsync(path.join(subpath, 'index.json'), JSON.stringify(subData)); | ||||
|   }); | ||||
| }; | ||||
| Accounts._setAcc = function (req, accId, acc) { | ||||
|   var accpath = Accounts._accPath(req, accId); | ||||
|   return mkdirpAsync(accpath).then(function () { | ||||
|     return sfs.writeFileAsync(path.join(accpath, 'index.json'), JSON.stringify(acc)); | ||||
|   }); | ||||
| }; | ||||
| Accounts.create = function (req) { | ||||
|   var id = Accounts._getTokenId(req.auth); | ||||
|   var acc = { | ||||
|     sub: crypto.randomBytes(16).toString('hex') | ||||
|          // TODO use something from the request to know which of the domains to use
 | ||||
|   , iss: req._state.config.webminDomain | ||||
|   , contacts: [] | ||||
|   }; | ||||
|   var accId = Accounts._getTokenId(acc); | ||||
|   acc.id = accId; | ||||
| 
 | ||||
|   // TODO notify any non-authorized accounts that they've been added?
 | ||||
|   return Accounts.getBySub(req).then(function (subData) { | ||||
|     subData.accounts.push({ type: 'self', id: accId }); | ||||
|     acc.contacts.push({ type: 'oauth3', id: subData.id, sub: subData.sub, iss: subData.iss }); | ||||
|     return Accounts._setSub(req, id, subData).then(function () { | ||||
|       return Accounts._setAcc(req, accId, acc).then(function () { | ||||
|         return acc; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| /* | ||||
| // TODO an owner of an asset can give permission to another entity
 | ||||
| // but that does not mean that that owner has access to that entity's things
 | ||||
| // Example:
 | ||||
| //   A 3rd party login's email verification cannot be trusted for auth
 | ||||
| //   Only 1st party verifications can be trusted for authorization
 | ||||
| Accounts.link = function (req) { | ||||
| }; | ||||
| */ | ||||
| Accounts.getBySub = function (req) { | ||||
|   var id = Accounts._getTokenId(req.auth); | ||||
|   var subpath = Accounts._subPath(req, id); | ||||
|   return readFileAsync(path.join(subpath, 'index.json'), 'utf8').then(function (text) { | ||||
|     return JSON.parse(text); | ||||
|   }, function (/*err*/) { | ||||
|     return null; | ||||
|   }).then(function (links) { | ||||
|     return links || { id: id, sub: req.auth.sub, iss: req.auth.iss, accounts: [] }; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function sendMail(state, auth) { | ||||
|   console.log('[DEBUG] ext auth', auth); | ||||
|   /* | ||||
| @ -132,8 +207,188 @@ function sendMail(state, auth) { | ||||
|         console.error(err); | ||||
|       } | ||||
|     }); | ||||
|     console.log("[DEBUG] email was sent, or so they say"); | ||||
|     console.log(resp.body); | ||||
|     // anything in the 200 range
 | ||||
|     if (2 === Math.floor(resp.statusCode / 100)) { | ||||
|       console.log("[DEBUG] email was sent, or so they say"); | ||||
|     } else { | ||||
|       console.error("[Error] email failed to send, or so they say:"); | ||||
|       console.error(resp.headers); | ||||
|       console.error(resp.statusCode, resp.body); | ||||
|       return PromiseA.reject(new Error("Error sending email: " + resp.statusCode + " " + resp.body)); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| // TODO replace with OAuth3 function
 | ||||
| function oauth3Auth(req, res, next) { | ||||
|   var jwt = require('jsonwebtoken'); | ||||
|   var verifyJwt = util.promisify(jwt.verify); | ||||
|   var token = (req.headers.authorization||'').replace(/^bearer /i, ''); | ||||
|   var auth; | ||||
|   var authData; | ||||
| 
 | ||||
|   if (!token) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: "E_NOAUTH" | ||||
|       , message: "no authorization header" | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     authData = jwt.decode(token, { complete: true }); | ||||
|   } catch(e) { | ||||
|     authData = null; | ||||
|   } | ||||
| 
 | ||||
|   if (!authData) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: "E_PARSEAUTH" | ||||
|       , message: "could not parse authorization header as JWT" | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   auth = authData.payload; | ||||
|   if (!auth.sub && ('*' === auth.aud || '*' === auth.azp)) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: "E_NOIMPL" | ||||
|       , message: "missing 'sub' and a wildcard 'azp' or 'aud' indicates that this is an exchange token," | ||||
|          + " however, this app has not yet implemented opaque token exchange" | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if ([ 'sub', 'iss' ].some(function (key) { | ||||
|     if ('string' !== typeof auth[key]) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_PARSEAUTH" | ||||
|         , message: "could not read property '" + key + "' of authorization token" | ||||
|         } | ||||
|       }); | ||||
|       return true; | ||||
|     } | ||||
|   })) { return; } | ||||
|   if ([ 'kid' ].some(function (key) { | ||||
|     if (/\/|\\/.test(authData.header[key])) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_PARSESUBJECT" | ||||
|         , message: "'" + key + "' `" + JSON.stringify(authData.header[key]) + "' contains invalid characters" | ||||
|         } | ||||
|       }); | ||||
|       return true; | ||||
|     } | ||||
|   })) { return; } | ||||
|   if ([ 'sub', 'kid' ].some(function (key) { | ||||
|     if (/\/|\\/.test(auth[key])) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_PARSESUBJECT" | ||||
|         , message: "'" + key + "' `" + JSON.stringify(auth[key]) + "' contains invalid characters" | ||||
|         } | ||||
|       }); | ||||
|       return true; | ||||
|     } | ||||
|   })) { return; } | ||||
| 
 | ||||
|   // TODO needs to work with app:// and custom://
 | ||||
|   function prefixHttps(str) { | ||||
|     return (str||'').replace(/^(https?:\/\/)?/i, 'https://'); | ||||
|   } | ||||
| 
 | ||||
|   var url = require('url'); | ||||
|   var discoveryUrl = url.resolve(prefixHttps(auth.iss), '_apis/oauth3.org/index.json'); | ||||
|   console.log('discoveryUrl: ', discoveryUrl, auth.iss); | ||||
|   return requestAsync({ | ||||
|     url: discoveryUrl | ||||
|   , json: true | ||||
|   }).then(function (resp) { | ||||
| 
 | ||||
|     // TODO
 | ||||
|     // it may be necessary to exchange the token,
 | ||||
| 
 | ||||
|     if (200 !== resp.statusCode || 'object' !== typeof resp.body || !resp.body.retrieve_jwk | ||||
|         || 'string' !== typeof resp.body.retrieve_jwk.url || 'string' !== typeof resp.body.api) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_NOTFOUND" | ||||
|         , message: resp.statusCode + ": issuer `" + JSON.stringify(auth.iss) | ||||
|             + "' does not declare 'api' & 'retrieve_key' and hence the token you provided cannot be verified." | ||||
|         , _status: resp.statusCode | ||||
|         , _url: discoveryUrl | ||||
|         , _body: resp.body | ||||
|         } | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     var keyUrl = url.resolve( | ||||
|         prefixHttps(resp.body.api).replace(/:hostname/g, auth.iss) | ||||
|       , resp.body.retrieve_jwk.url | ||||
|           .replace(/:hostname/g, auth.iss) | ||||
|           .replace(/:sub/g, auth.sub) | ||||
|           // TODO
 | ||||
|           .replace(/:kid/g, authData.header.kid || auth.iss) | ||||
|     ); | ||||
|     console.log('keyUrl: ', keyUrl); | ||||
|     return requestAsync({ | ||||
|       url: keyUrl | ||||
|     , json: true | ||||
|     }).then(function (resp) { | ||||
|       var jwk = resp.body; | ||||
|       if (200 !== resp.statusCode || 'object' !== typeof resp.body) { | ||||
|         //headers.authorization
 | ||||
|         res.send({ | ||||
|           error: { | ||||
|             code: "E_NOTFOUND" | ||||
|           , message: resp.statusCode + ": did not retrieve public key from `" + JSON.stringify(auth.iss) | ||||
|               + "' for token validation and hence the token you provided cannot be verified." | ||||
|           , _status: resp.statusCode | ||||
|           , _url: keyUrl | ||||
|           , _body: resp.body | ||||
|           } | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       var pubpem; | ||||
|       try { | ||||
|         pubpem = require('jwk-to-pem')(jwk, { private: false }); | ||||
|       } catch(e) { | ||||
|         pubpem = null; | ||||
|       } | ||||
| 			return verifyJwt(token, pubpem, { | ||||
| 				algorithms: [ 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512' ] | ||||
| 			}).then(function (decoded) { | ||||
|         if (!decoded) { | ||||
|           res.send({ | ||||
|             error: { | ||||
|               code: "E_UNVERIFIED" | ||||
|             , message: "retrieved jwk does not verify provided token." | ||||
|             , _jwk: jwk | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|         req.auth = {}; | ||||
|         req.auth.jwt = token; | ||||
|         req.auth.data = auth; | ||||
|         next(); | ||||
| 			}); | ||||
|     }); | ||||
|   }, function (err) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: err.code || "E_GENERIC" | ||||
|       , message: err.toString() | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| @ -156,20 +411,24 @@ module.exports.pairRequest = function (opts) { | ||||
|     , aud: state.config.webminDomain | ||||
|     , iat: Math.round(now / 1000) | ||||
|     , id: authReq.id | ||||
|     , sub: authReq.subject | ||||
|     , pin: pin | ||||
|     , hostname: authReq.hostname | ||||
|     }; | ||||
|     auth = { | ||||
|       id: authReq.id | ||||
|     , secret: authReq.secret | ||||
|     , subject: authReq.subject | ||||
|     , pin: pin | ||||
|     , dt: now | ||||
|     , exp: now + (2 * 60 * 60 * 1000) | ||||
|     , authnData: authnData | ||||
|     , authn: jwt.sign(authnData, state.secret) | ||||
|     , request: authReq | ||||
|     }; | ||||
| 
 | ||||
|     // Setting extra authnData
 | ||||
|     auth.authn = jwt.sign(authnData, state.secret); | ||||
|     authnData.jwt = auth.authn; | ||||
|     auth.authnData = authnData; | ||||
|     Auths.set(auth, authReq.id, authReq.secret); | ||||
|     return authnData; | ||||
|   }); | ||||
| @ -179,16 +438,23 @@ module.exports.pairPin = function (opts) { | ||||
|   return state.Promise.resolve().then(function () { | ||||
|     var pin = opts.pin; | ||||
|     var secret = opts.secret; | ||||
|     var auth = Auths.getBySecretAndPin(secret, pin); | ||||
|     var auth = Auths.getBySecret(secret); | ||||
| 
 | ||||
|     console.log('[pairPin] validating secret and pin'); | ||||
|     if (!auth) { | ||||
|       throw new Error("I can't even right now - bad magic link or pairing code"); | ||||
|       throw new Error("Invalid magic link token '" + secret + "'"); | ||||
|     } | ||||
|     auth = Auths.getBySecretAndPin(secret, pin); | ||||
|     if (!auth) { | ||||
|       throw new Error("Invalid pairing code '" + pin + "' for magic link token '" + secret + "'"); | ||||
|     } | ||||
| 
 | ||||
|     if (auth._offered) { | ||||
|       console.log('[pairPin] already has offer to return'); | ||||
|       return auth._offered; | ||||
|     } | ||||
| 
 | ||||
|     console.log('[pairPin] generating offer'); | ||||
|     var hri = require('human-readable-ids').hri; | ||||
|     var hrname = hri.random() + '.' + state.config.sharedDomain; | ||||
|     // TODO check used / unused names and ports
 | ||||
| @ -202,9 +468,14 @@ module.exports.pairPin = function (opts) { | ||||
|     }; | ||||
|     var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data'); | ||||
|     auth.authz = jwt.sign(authzData, state.secret); | ||||
|     auth.authzData = authzData; | ||||
|     authzData.jwt = auth.authz; | ||||
|     auth._offered = authzData; | ||||
|     if (auth.resolve) { | ||||
|       console.log('[pairPin] resolving'); | ||||
|       auth.resolve(auth); | ||||
|     } else { | ||||
|       console.log('[pairPin] not resolvable'); | ||||
|     } | ||||
|     fs.writeFile(pathname, JSON.stringify(authzData), function (err) { | ||||
|       if (err) { | ||||
| @ -212,16 +483,32 @@ module.exports.pairPin = function (opts) { | ||||
|         console.error(err); | ||||
|       } | ||||
|     }); | ||||
|     auth._offered = authzData; | ||||
|     return authzData; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| // From a WS connection
 | ||||
| module.exports.authHelper = function (meta) { | ||||
|   console.log('[authHelper] 1'); | ||||
|   var state = meta.state; | ||||
|   console.log('[authHelper] 2'); | ||||
|   return state.Promise.resolve().then(function () { | ||||
|     console.log('[authHelper] 3'); | ||||
|     var auth = meta.session; | ||||
|     console.log('[authHelper] 4', auth); | ||||
|     if (!auth || 'string' !== typeof auth.authz || 'object' !== typeof auth.authzData) { | ||||
|       console.log('[authHelper] 5'); | ||||
|       console.error("[SANITY FAIL] should not complete auth without authz data and access_token"); | ||||
|       console.error(auth); | ||||
|       return; | ||||
|     } | ||||
|     console.log("[authHelper] passing authzData right along", auth.authzData); | ||||
|     return auth.authzData; | ||||
|   }); | ||||
| }; | ||||
| // opts = { state: state, auth: auth_request OR access_token }
 | ||||
| module.exports.authenticate = function (opts) { | ||||
|   var jwt = require('jsonwebtoken'); | ||||
|   var jwtoken = opts.auth; | ||||
|   var authReq = opts.auth; | ||||
|   var state = opts.state; | ||||
|   var auth; | ||||
|   var decoded; | ||||
| @ -241,7 +528,6 @@ module.exports.authenticate = function (opts) { | ||||
|       // this will cause the websocket to disconnect
 | ||||
| 
 | ||||
|       auth.resolve = function (auth) { | ||||
|         opts.auth = auth.authz; | ||||
|         auth.resolve = null; | ||||
|         auth.reject = null; | ||||
|         // NOTE XXX: This is premature in the sense that we can't be 100% sure
 | ||||
| @ -249,7 +535,12 @@ module.exports.authenticate = function (opts) { | ||||
|         // sort of check that the client actually received the token
 | ||||
|         // (i.e. when the grant event gets an ack)
 | ||||
|         auth._claimed = true; | ||||
|         return state.defaults.authenticate(opts.auth).then(resolve); | ||||
|         // this is probably not necessary anymore
 | ||||
|         opts.auth = auth.authz; | ||||
|         return module.exports.authHelper({ | ||||
|           state: state | ||||
|         , session: auth | ||||
|         }).then(resolve); | ||||
|       }; | ||||
|       auth.reject = function (err) { | ||||
|         auth.resolve = null; | ||||
| @ -261,41 +552,43 @@ module.exports.authenticate = function (opts) { | ||||
|     return auth.promise; | ||||
|   } | ||||
| 
 | ||||
|   if ('object' === typeof authReq && /^.+@.+\..+$/.test(authReq.subject)) { | ||||
|     console.log("[ext token] Looks Like Auth Object"); | ||||
|   // Promise Authz on Auth Creds
 | ||||
|   // TODO: remove
 | ||||
|   if ('object' === typeof opts.auth && /^.+@.+\..+$/.test(opts.auth.subject)) { | ||||
|     console.log("[wss.ext.authenticate] [1] Request Pair for Credentials"); | ||||
|     return module.exports.pairRequest(opts).then(function (authnData) { | ||||
|       console.log("[ext token] Promises Like Auth Object"); | ||||
|       console.log("[wss.ext.authenticate] [2] Promise Authz on Pair Complete"); | ||||
|       var auth = Auths.get(authnData.id); | ||||
|       return getPromise(auth); | ||||
|       //getPromise(auth);
 | ||||
|       //return state.defaults.authenticate(authnData.jwt);
 | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   console.log("[ext token] Trying Token Parse"); | ||||
|   try { | ||||
|     decoded = jwt.decode(jwtoken, { complete: true }); | ||||
|     decoded = jwt.decode(opts.auth, { complete: true }); | ||||
|     auth = Auths.get(decoded.payload.id); | ||||
|   } catch(e) { | ||||
|     console.log("[ext token] Token Did Not Parse"); | ||||
|     console.log("[wss.ext.authenticate] [Error] could not parse token"); | ||||
|     decoded = null; | ||||
|   } | ||||
| 
 | ||||
|   console.log("[ext token] decoded auth token:"); | ||||
|   console.log("[wss.ext.authenticate] incoming token decoded:"); | ||||
|   console.log(decoded); | ||||
| 
 | ||||
|   if (!auth) { | ||||
|     console.log("[ext token] did not find auth object"); | ||||
|     console.log("[wss.ext.authenticate] no session / auth handshake. Pass to default auth"); | ||||
|     return state.defaults.authenticate(opts.auth); | ||||
|   } | ||||
| 
 | ||||
|   // TODO technically this could leak the token through a timing attack
 | ||||
|   // but it would require already knowing the semi-secret id and having
 | ||||
|   // completed the pair code
 | ||||
|   if (auth && (auth.authn === jwtoken || auth.authz === jwtoken)) { | ||||
|   if (auth.authn === opts.auth || auth.authz === opts.auth) { | ||||
|     if (!auth.authz) { | ||||
|       console.log("[ext token] Promise Authz"); | ||||
|       console.log("[wss.ext.authenticate] Create authz promise and passthru"); | ||||
|       return getPromise(auth); | ||||
|     } | ||||
| 
 | ||||
|     console.log("[ext token] Use Available Authz"); | ||||
|     // If they used authn but now authz is available, use authz
 | ||||
|     // (i.e. connects, but no domains or ports)
 | ||||
|     opts.auth = auth.authz; | ||||
| @ -304,8 +597,8 @@ module.exports.authenticate = function (opts) { | ||||
|     auth._claimed = true; | ||||
|   } | ||||
| 
 | ||||
|   console.log("[ext token] Continue With Auth Token"); | ||||
|   return state.defaults.authenticate(opts.auth); | ||||
|   console.log("[wss.ext.authenticate] Already using authz, skipping promise"); | ||||
|   return module.exports.authHelper({ state: state, session: auth }); | ||||
| }; | ||||
| 
 | ||||
| //var loaded = false;
 | ||||
| @ -319,9 +612,36 @@ var urls = { | ||||
|   pairState: '/api/telebit.cloud/pair_state/:id' | ||||
| }; | ||||
| staticApp.use('/', express.static(path.join(__dirname, 'admin'))); | ||||
| app.use('/api', CORS({})); | ||||
| app.use('/api', CORS({ | ||||
|   credentials: true | ||||
| , headers: [ 'Authorization', 'X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept' ] | ||||
| })); | ||||
| app.use('/api', bodyParser.json()); | ||||
| 
 | ||||
| app.use('/api/telebit.cloud/account', oauth3Auth); | ||||
| app.get('/api/telebit.cloud/account', function (req, res) { | ||||
|   Accounts.getBySub(req).then(function (subData) { | ||||
|     res.send(subData); | ||||
|   }, function (err) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: err.code || "E_GENERIC" | ||||
|       , message: err.toString() | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| app.post('/api/telebit.cloud/account', function (req, res) { | ||||
|   return Accounts.create(req).then(function (acc) { | ||||
|     res.send({ | ||||
|       success: true | ||||
|     , id: acc.id | ||||
|     , sub: acc.sub | ||||
|     , iss: acc.iss | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| // From Device (which knows id, but not secret)
 | ||||
| app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||
|   var auth = req.body; | ||||
| @ -345,7 +665,7 @@ app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||
| app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) { | ||||
|   var secret = req.params.secret; | ||||
|   var auth = Auths.getBySecret(secret); | ||||
|   var crypto = require('crypto'); | ||||
|   //var crypto = require('crypto');
 | ||||
|   var response = {}; | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										102
									
								
								lib/relay.js
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								lib/relay.js
									
									
									
									
									
								
							| @ -1,10 +1,15 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var url = require('url'); | ||||
| var PromiseA = require('bluebird'); | ||||
| var sni = require('sni'); | ||||
| var Packer = require('proxy-packer'); | ||||
| var PortServers = {}; | ||||
| var PromiseA; | ||||
| try { | ||||
|   PromiseA = require('bluebird'); | ||||
| } catch(e) { | ||||
|   PromiseA = global.Promise; | ||||
| } | ||||
| 
 | ||||
| function timeoutPromise(duration) { | ||||
|   return new PromiseA(function (resolve) { | ||||
| @ -240,10 +245,10 @@ var Server = { | ||||
| 
 | ||||
|     return result || srv.socketId; | ||||
|   } | ||||
| , onAuth: function onAuth(state, srv, newAuth, grant) { | ||||
| , onAuth: function onAuth(state, srv, rawAuth, grant) { | ||||
|     console.log('\n[relay.js] onAuth'); | ||||
|     console.log(newAuth); | ||||
|     console.log(grant); | ||||
|     console.log(rawAuth); | ||||
|     //console.log(grant);
 | ||||
|     //var stringauth;
 | ||||
|     var err; | ||||
|     if (!grant || 'object' !== typeof grant) { | ||||
| @ -253,25 +258,37 @@ var Server = { | ||||
|       return state.Promise.reject(err); | ||||
|     } | ||||
| 
 | ||||
|     if ('string' !== typeof newAuth) { | ||||
|       newAuth = JSON.stringify(newAuth); | ||||
|     if ('string' !== typeof rawAuth) { | ||||
|       rawAuth = JSON.stringify(rawAuth); | ||||
|     } | ||||
| 
 | ||||
|     console.log('check for upgrade token'); | ||||
|     if (grant.jwt && newAuth !== grant.jwt) { | ||||
|       console.log('new token to send back'); | ||||
|       // Access Token
 | ||||
|       Server.sendTunnelMsg( | ||||
|         srv | ||||
|       , null | ||||
|       , [ 3 | ||||
|         , 'access_token' | ||||
|         , { jwt: grant.jwt } | ||||
|         ] | ||||
|       , 'control' | ||||
|       ); | ||||
|       // these aren't needed internally once they're sent
 | ||||
|       grant.jwt = null; | ||||
|     // TODO don't fire the onAuth event on non-authz updates
 | ||||
|     if (!grant.jwt && !(grant.domains||[]).length && !(grant.ports||[]).length) { | ||||
|       console.log("[onAuth] nothing to offer at all"); | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     console.log('[onAuth] check for upgrade token'); | ||||
|     //console.log(grant);
 | ||||
|     if (grant.jwt) { | ||||
|       if (rawAuth !== grant.jwt) { | ||||
|         console.log('[onAuth] new token to send back'); | ||||
|       } | ||||
|       // TODO only send token when new
 | ||||
|       if (true) { | ||||
|         // Access Token
 | ||||
|         Server.sendTunnelMsg( | ||||
|           srv | ||||
|         , null | ||||
|         , [ 3 | ||||
|           , 'access_token' | ||||
|           , { jwt: grant.jwt } | ||||
|           ] | ||||
|         , 'control' | ||||
|         ); | ||||
|         // these aren't needed internally once they're sent
 | ||||
|         grant.jwt = null; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
| @ -288,18 +305,20 @@ var Server = { | ||||
|       return state.Promise.reject(err); | ||||
|     } | ||||
| 
 | ||||
|     console.log('strolling through pleasantries'); | ||||
|     console.log('[onAuth] strolling through pleasantries'); | ||||
|     // Add the custom properties we need to manage this remote, then add it to all the relevant
 | ||||
|     // domains and the list of all this websocket's grants.
 | ||||
|     grant.domains.forEach(function (domainname) { | ||||
|       console.log('add', domainname, 'to device lists'); | ||||
|       srv.domainsMap[domainname] = true; | ||||
|       Devices.add(state.deviceLists, domainname, srv); | ||||
|       // TODO allow subs to go to individual devices
 | ||||
|       Devices.alias(state.deviceLists, domainname, '*.' + domainname); | ||||
|     }); | ||||
|     srv.domains = Object.keys(srv.domainsMap); | ||||
|     srv.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || srv.domains.join(','); | ||||
|     grant.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || grant.domains.join(','); | ||||
|     grant.srv = srv; | ||||
|     //grant.srv = srv;
 | ||||
|     //grant.ws = srv.ws;
 | ||||
|     //grant.upgradeReq = srv.upgradeReq;
 | ||||
|     grant.clients = {}; | ||||
| @ -344,7 +363,7 @@ var Server = { | ||||
|     } | ||||
|     grant.ports.forEach(openPort); | ||||
| 
 | ||||
|     srv.grants[newAuth] = grant; | ||||
|     srv.grants[rawAuth] = grant; | ||||
|     console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc); | ||||
| 
 | ||||
|     console.log('notify of grants', grant.domains, grant.ports); | ||||
| @ -416,31 +435,35 @@ var Server = { | ||||
|       process.nextTick(function () { conn.resume(); }); | ||||
|     }); | ||||
|   } | ||||
| , addToken: function addToken(state, srv, newAuth) { | ||||
|     console.log("addToken", newAuth); | ||||
|     if (srv.grants[newAuth]) { | ||||
| , addToken: function addToken(state, srv, rawAuth) { | ||||
|     console.log("[addToken]", rawAuth); | ||||
|     if (srv.grants[rawAuth]) { | ||||
|       console.log("addToken - duplicate"); | ||||
|       // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | ||||
|       return state.Promise.resolve(null); | ||||
|     } | ||||
| 
 | ||||
|     return state.authenticate({ auth: newAuth }).then(function (authnToken) { | ||||
| 
 | ||||
|       console.log('\n[relay.js] newAuth'); | ||||
|       console.log(newAuth); | ||||
|     return state.authenticate({ auth: rawAuth }).then(function (validatedTokenData) { | ||||
|       console.log('\n[relay.js] rawAuth'); | ||||
|       console.log(rawAuth); | ||||
| 
 | ||||
|       console.log('\n[relay.js] authnToken'); | ||||
|       console.log(authnToken); | ||||
|       console.log(validatedTokenData); | ||||
| 
 | ||||
|       if (authnToken.id) { | ||||
|         state.srvs[authnToken.id] = state.srvs[authnToken.id] || {}; | ||||
|         state.srvs[authnToken.id].updateAuth = function (validToken) { | ||||
|           return Server.onAuth(state, srv, newAuth, validToken); | ||||
|       // For tracking state between token exchanges
 | ||||
|       // and tacking on extra attributes (i.e. for extensions)
 | ||||
|       // TODO close on delete
 | ||||
|       if (!state.srvs[validatedTokenData.id]) { | ||||
|         state.srvs[validatedTokenData.id] = {}; | ||||
|       } | ||||
|       if (!state.srvs[validatedTokenData.id].updateAuth) { | ||||
|         // be sure to always pass latest srv since the connection may change
 | ||||
|         // and reuse the same token
 | ||||
|         state.srvs[validatedTokenData.id].updateAuth = function (srv, validatedTokenData) { | ||||
|           return Server.onAuth(state, srv, rawAuth, validatedTokenData); | ||||
|         }; | ||||
|       } | ||||
| 
 | ||||
|       // will return rejection if necessary
 | ||||
|       return state.srvs[authnToken.id].updateAuth(authnToken); | ||||
|       state.srvs[validatedTokenData.id].updateAuth(srv, validatedTokenData); | ||||
|     }); | ||||
|   } | ||||
| , removeToken: function removeToken(state, srv, jwtoken) { | ||||
| @ -587,6 +610,7 @@ module.exports.create = function (state) { | ||||
|     }); | ||||
| 
 | ||||
|     if (initToken) { | ||||
|       console.log('[wss.onConnection] token provided in http headers'); | ||||
|       return Server.addToken(state, srv, initToken).then(function () { | ||||
|         Server.init(state, srv); | ||||
|       }).catch(function (err) { | ||||
|  | ||||
| @ -46,6 +46,12 @@ module.exports.createTcpConnectionHandler = function (state) { | ||||
|       function tryTls() { | ||||
|         var vhost; | ||||
| 
 | ||||
|         if (!servername) { | ||||
|           if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); } | ||||
|           deferData('httpsInvalid'); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if (!state.servernames.length) { | ||||
|           console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)"); | ||||
|           deferData('httpsSetupServer'); | ||||
| @ -62,12 +68,6 @@ module.exports.createTcpConnectionHandler = function (state) { | ||||
|           console.log("TODO: use www bare redirect"); | ||||
|         } | ||||
| 
 | ||||
|         if (!servername) { | ||||
|           if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); } | ||||
|           deferData('httpsInvalid'); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         function run() { | ||||
|           var nextDevice = Devices.next(state.deviceLists, servername); | ||||
|           if (!nextDevice) { | ||||
|  | ||||
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @ -37,8 +37,7 @@ | ||||
|   }, | ||||
|   "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js", | ||||
|   "dependencies": { | ||||
|     "@coolaj86/urequest": "^1.1.1", | ||||
|     "bluebird": "^3.5.1", | ||||
|     "@coolaj86/urequest": "^1.3.2", | ||||
|     "body-parser": "^1.18.3", | ||||
|     "cluster-store": "^2.0.8", | ||||
|     "connect-cors": "^0.5.6", | ||||
| @ -48,16 +47,22 @@ | ||||
|     "greenlock": "^2.2.4", | ||||
|     "human-readable-ids": "^1.0.4", | ||||
|     "js-yaml": "^3.11.0", | ||||
|     "jsonwebtoken": "^8.2.1", | ||||
|     "jsonwebtoken": "^8.3.0", | ||||
|     "jwk-to-pem": "^2.0.0", | ||||
|     "mkdirp": "^0.5.1", | ||||
|     "nowww": "^1.2.1", | ||||
|     "proxy-packer": "^1.4.3", | ||||
|     "recase": "^1.0.4", | ||||
|     "redirect-https": "^1.1.5", | ||||
|     "request": "^2.87.0", | ||||
|     "safe-replace": "^1.0.3", | ||||
|     "serve-static": "^1.13.2", | ||||
|     "sni": "^1.0.0", | ||||
|     "ws": "^5.1.1" | ||||
|   }, | ||||
|   "trulyOptionalDependencies": { | ||||
|     "bluebird": "^3.5.1" | ||||
|   }, | ||||
|   "engineStrict": true, | ||||
|   "engines": { | ||||
|     "node": "10.2.1" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user