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) | email: coolaj86@gmail.com | ||||||
| agree_tos: true                # agree to the Telebit, Greenlock, and Let's Encrypt TOSes | agree_tos: true | ||||||
| community_member: true         # receive infrequent relevant updates | community_member: true | ||||||
| telemetry: true                # contribute to project telemetric data | telemetry: true | ||||||
| webmin_domain: example.com | webmin_domain: example.com | ||||||
| shared_domain: xm.pl | api_domain: example.com | ||||||
| servernames:                   # hostnames that direct to the Telebit Relay admin console | shared_domain: example.com | ||||||
|   - telebit.example.com | servernames: | ||||||
|   - telebit.example.net |   - www.example.com | ||||||
| vhost: /srv/www/:hostname      # load secure websites at this path (uses template string, i.e. /var/www/:hostname/public) |   - example.com | ||||||
|  |   - api.example.com | ||||||
|  | vhost: /srv/www/:hostname | ||||||
| greenlock: | greenlock: | ||||||
|   version: 'draft-11' |   version: 'draft-11' | ||||||
|   server: 'https://acme-v02.api.letsencrypt.org/directory' |   server: 'https://acme-v02.api.letsencrypt.org/directory' | ||||||
|   store: |   store: | ||||||
|     strategy: le-store-certbot # certificate storage plugin |     strategy: le-store-certbot | ||||||
|   config_dir: /etc/acme        # directory for ssl certificates |   config_dir: /opt/telebit-relay/etc/acme | ||||||
| secret: ''                     # generate with node -e "console.log(crypto.randomBytes(16).toString('hex'))" | secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
| mailer: | mailer: | ||||||
|   url: 'https://api.mailgun.net/v3/EXAMPLE.COM/messages' |   url: 'https://api.mailgun.net/v3/EXAMPLE.COM/messages' | ||||||
|   api_key: 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' |   api_key: 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | ||||||
|   from: 'Example Mailer <MALIER@EXAMPLE.COM>' |   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 '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 '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 '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" |   sudo bash -c "cat $TELEBIT_RELAY_PATH/examples/$my_app.yml.tpl >> $TELEBIT_RELAY_PATH/etc/$my_app.yml" | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,9 +2,24 @@ | |||||||
| 
 | 
 | ||||||
| var Devices = module.exports; | var Devices = module.exports; | ||||||
| Devices.add = function (store, servername, newDevice) { | Devices.add = function (store, servername, newDevice) { | ||||||
|   var devices = store[servername] || []; |   if (!store[servername]) { | ||||||
|  |     store[servername] = []; | ||||||
|  |   } | ||||||
|  |   var devices = store[servername]; | ||||||
|   devices.push(newDevice); |   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) { | Devices.remove = function (store, servername, device) { | ||||||
|   var devices = store[servername] || []; |   var devices = store[servername] || []; | ||||||
| @ -17,9 +32,11 @@ Devices.remove = function (store, servername, device) { | |||||||
|   return devices.splice(index, 1)[0]; |   return devices.splice(index, 1)[0]; | ||||||
| }; | }; | ||||||
| Devices.list = function (store, servername) { | Devices.list = function (store, servername) { | ||||||
|  |   // efficient lookup first
 | ||||||
|   if (store[servername] && store[servername].length) { |   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
 |   // 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.
 |   // first so the one with the biggest natural match with be found first.
 | ||||||
|   var deviceList = []; |   var deviceList = []; | ||||||
| @ -28,10 +45,19 @@ Devices.list = function (store, servername) { | |||||||
|   }).sort(function (a, b) { |   }).sort(function (a, b) { | ||||||
|     return b.length - a.length; |     return b.length - a.length; | ||||||
|   }).some(function (pattern) { |   }).some(function (pattern) { | ||||||
|  |     // '.example.com' = '*.example.com'.split(1)
 | ||||||
|     var subPiece = pattern.slice(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)) { |     if (subPiece === servername.slice(-subPiece.length)) { | ||||||
|       console.log('"'+servername+'" matches "'+pattern+'"'); |       console.log('[Devices.list] "'+servername+'" matches "'+pattern+'"'); | ||||||
|       deviceList = store[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; |       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> |       <p>Friends enable friends to share anything, access anywhere, connect anytime.</p> | ||||||
|     </center> |     </center> | ||||||
| 
 | 
 | ||||||
|  |     <a href="account.html#/login">Login</a> | ||||||
|  |     <a href="account.html#/create_account">Create Account</a> | ||||||
|  | 
 | ||||||
|     <div style="width: 800px; margin: auto;"> |     <div style="width: 800px; margin: auto;"> | ||||||
|       <div> |       <div> | ||||||
|         <h2>Share and Test over HTTPS</h2> |         <h2>Share and Test over HTTPS</h2> | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ function checkStatus() { | |||||||
|       } |       } | ||||||
|       if ('complete' === data.status) { |       if ('complete' === data.status) { | ||||||
|         setTimeout(function () { |         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,
 |           // TODO once this is loaded (even error) Let's Encrypt is done,
 | ||||||
|           // then it's time to redirect to the domain. Yay!
 |           // then it's time to redirect to the domain. Yay!
 | ||||||
|         }, 1 * 1000); |         }, 1 * 1000); | ||||||
|  | |||||||
| @ -6,7 +6,16 @@ var util = require('util'); | |||||||
| var crypto = require('crypto'); | var crypto = require('crypto'); | ||||||
| var escapeHtml = require('escape-html'); | var escapeHtml = require('escape-html'); | ||||||
| var jwt = require('jsonwebtoken'); | 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 = module.exports._auths = {}; | ||||||
| var 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) { | function sendMail(state, auth) { | ||||||
|   console.log('[DEBUG] ext auth', auth); |   console.log('[DEBUG] ext auth', auth); | ||||||
|   /* |   /* | ||||||
| @ -132,8 +207,188 @@ function sendMail(state, auth) { | |||||||
|         console.error(err); |         console.error(err); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     // anything in the 200 range
 | ||||||
|  |     if (2 === Math.floor(resp.statusCode / 100)) { | ||||||
|       console.log("[DEBUG] email was sent, or so they say"); |       console.log("[DEBUG] email was sent, or so they say"); | ||||||
|     console.log(resp.body); |     } 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 |     , aud: state.config.webminDomain | ||||||
|     , iat: Math.round(now / 1000) |     , iat: Math.round(now / 1000) | ||||||
|     , id: authReq.id |     , id: authReq.id | ||||||
|  |     , sub: authReq.subject | ||||||
|     , pin: pin |     , pin: pin | ||||||
|     , hostname: authReq.hostname |     , hostname: authReq.hostname | ||||||
|     }; |     }; | ||||||
|     auth = { |     auth = { | ||||||
|       id: authReq.id |       id: authReq.id | ||||||
|     , secret: authReq.secret |     , secret: authReq.secret | ||||||
|  |     , subject: authReq.subject | ||||||
|     , pin: pin |     , pin: pin | ||||||
|     , dt: now |     , dt: now | ||||||
|     , exp: now + (2 * 60 * 60 * 1000) |     , exp: now + (2 * 60 * 60 * 1000) | ||||||
|     , authnData: authnData |  | ||||||
|     , authn: jwt.sign(authnData, state.secret) |  | ||||||
|     , request: authReq |     , request: authReq | ||||||
|     }; |     }; | ||||||
|  | 
 | ||||||
|  |     // Setting extra authnData
 | ||||||
|  |     auth.authn = jwt.sign(authnData, state.secret); | ||||||
|     authnData.jwt = auth.authn; |     authnData.jwt = auth.authn; | ||||||
|  |     auth.authnData = authnData; | ||||||
|     Auths.set(auth, authReq.id, authReq.secret); |     Auths.set(auth, authReq.id, authReq.secret); | ||||||
|     return authnData; |     return authnData; | ||||||
|   }); |   }); | ||||||
| @ -179,16 +438,23 @@ module.exports.pairPin = function (opts) { | |||||||
|   return state.Promise.resolve().then(function () { |   return state.Promise.resolve().then(function () { | ||||||
|     var pin = opts.pin; |     var pin = opts.pin; | ||||||
|     var secret = opts.secret; |     var secret = opts.secret; | ||||||
|     var auth = Auths.getBySecretAndPin(secret, pin); |     var auth = Auths.getBySecret(secret); | ||||||
| 
 | 
 | ||||||
|  |     console.log('[pairPin] validating secret and pin'); | ||||||
|     if (!auth) { |     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) { |     if (auth._offered) { | ||||||
|  |       console.log('[pairPin] already has offer to return'); | ||||||
|       return auth._offered; |       return auth._offered; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     console.log('[pairPin] generating offer'); | ||||||
|     var hri = require('human-readable-ids').hri; |     var hri = require('human-readable-ids').hri; | ||||||
|     var hrname = hri.random() + '.' + state.config.sharedDomain; |     var hrname = hri.random() + '.' + state.config.sharedDomain; | ||||||
|     // TODO check used / unused names and ports
 |     // 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'); |     var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data'); | ||||||
|     auth.authz = jwt.sign(authzData, state.secret); |     auth.authz = jwt.sign(authzData, state.secret); | ||||||
|  |     auth.authzData = authzData; | ||||||
|     authzData.jwt = auth.authz; |     authzData.jwt = auth.authz; | ||||||
|  |     auth._offered = authzData; | ||||||
|     if (auth.resolve) { |     if (auth.resolve) { | ||||||
|  |       console.log('[pairPin] resolving'); | ||||||
|       auth.resolve(auth); |       auth.resolve(auth); | ||||||
|  |     } else { | ||||||
|  |       console.log('[pairPin] not resolvable'); | ||||||
|     } |     } | ||||||
|     fs.writeFile(pathname, JSON.stringify(authzData), function (err) { |     fs.writeFile(pathname, JSON.stringify(authzData), function (err) { | ||||||
|       if (err) { |       if (err) { | ||||||
| @ -212,16 +483,32 @@ module.exports.pairPin = function (opts) { | |||||||
|         console.error(err); |         console.error(err); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|     auth._offered = authzData; |  | ||||||
|     return authzData; |     return authzData; | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // From a WS connection
 | // 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) { | module.exports.authenticate = function (opts) { | ||||||
|   var jwt = require('jsonwebtoken'); |   var jwt = require('jsonwebtoken'); | ||||||
|   var jwtoken = opts.auth; |  | ||||||
|   var authReq = opts.auth; |  | ||||||
|   var state = opts.state; |   var state = opts.state; | ||||||
|   var auth; |   var auth; | ||||||
|   var decoded; |   var decoded; | ||||||
| @ -241,7 +528,6 @@ module.exports.authenticate = function (opts) { | |||||||
|       // this will cause the websocket to disconnect
 |       // this will cause the websocket to disconnect
 | ||||||
| 
 | 
 | ||||||
|       auth.resolve = function (auth) { |       auth.resolve = function (auth) { | ||||||
|         opts.auth = auth.authz; |  | ||||||
|         auth.resolve = null; |         auth.resolve = null; | ||||||
|         auth.reject = null; |         auth.reject = null; | ||||||
|         // NOTE XXX: This is premature in the sense that we can't be 100% sure
 |         // 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
 |         // sort of check that the client actually received the token
 | ||||||
|         // (i.e. when the grant event gets an ack)
 |         // (i.e. when the grant event gets an ack)
 | ||||||
|         auth._claimed = true; |         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.reject = function (err) { | ||||||
|         auth.resolve = null; |         auth.resolve = null; | ||||||
| @ -261,41 +552,43 @@ module.exports.authenticate = function (opts) { | |||||||
|     return auth.promise; |     return auth.promise; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if ('object' === typeof authReq && /^.+@.+\..+$/.test(authReq.subject)) { |   // Promise Authz on Auth Creds
 | ||||||
|     console.log("[ext token] Looks Like Auth Object"); |   // 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) { |     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); |       var auth = Auths.get(authnData.id); | ||||||
|       return getPromise(auth); |       return getPromise(auth); | ||||||
|  |       //getPromise(auth);
 | ||||||
|  |       //return state.defaults.authenticate(authnData.jwt);
 | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   console.log("[ext token] Trying Token Parse"); |  | ||||||
|   try { |   try { | ||||||
|     decoded = jwt.decode(jwtoken, { complete: true }); |     decoded = jwt.decode(opts.auth, { complete: true }); | ||||||
|     auth = Auths.get(decoded.payload.id); |     auth = Auths.get(decoded.payload.id); | ||||||
|   } catch(e) { |   } catch(e) { | ||||||
|     console.log("[ext token] Token Did Not Parse"); |     console.log("[wss.ext.authenticate] [Error] could not parse token"); | ||||||
|     decoded = null; |     decoded = null; | ||||||
|   } |   } | ||||||
| 
 |   console.log("[wss.ext.authenticate] incoming token decoded:"); | ||||||
|   console.log("[ext token] decoded auth token:"); |  | ||||||
|   console.log(decoded); |   console.log(decoded); | ||||||
| 
 | 
 | ||||||
|   if (!auth) { |   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
 |   // TODO technically this could leak the token through a timing attack
 | ||||||
|   // but it would require already knowing the semi-secret id and having
 |   // but it would require already knowing the semi-secret id and having
 | ||||||
|   // completed the pair code
 |   // completed the pair code
 | ||||||
|   if (auth && (auth.authn === jwtoken || auth.authz === jwtoken)) { |   if (auth.authn === opts.auth || auth.authz === opts.auth) { | ||||||
|     if (!auth.authz) { |     if (!auth.authz) { | ||||||
|       console.log("[ext token] Promise Authz"); |       console.log("[wss.ext.authenticate] Create authz promise and passthru"); | ||||||
|       return getPromise(auth); |       return getPromise(auth); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     console.log("[ext token] Use Available Authz"); |  | ||||||
|     // If they used authn but now authz is available, use authz
 |     // If they used authn but now authz is available, use authz
 | ||||||
|     // (i.e. connects, but no domains or ports)
 |     // (i.e. connects, but no domains or ports)
 | ||||||
|     opts.auth = auth.authz; |     opts.auth = auth.authz; | ||||||
| @ -304,8 +597,8 @@ module.exports.authenticate = function (opts) { | |||||||
|     auth._claimed = true; |     auth._claimed = true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   console.log("[ext token] Continue With Auth Token"); |   console.log("[wss.ext.authenticate] Already using authz, skipping promise"); | ||||||
|   return state.defaults.authenticate(opts.auth); |   return module.exports.authHelper({ state: state, session: auth }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| //var loaded = false;
 | //var loaded = false;
 | ||||||
| @ -319,9 +612,36 @@ var urls = { | |||||||
|   pairState: '/api/telebit.cloud/pair_state/:id' |   pairState: '/api/telebit.cloud/pair_state/:id' | ||||||
| }; | }; | ||||||
| staticApp.use('/', express.static(path.join(__dirname, 'admin'))); | 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', 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)
 | // From Device (which knows id, but not secret)
 | ||||||
| app.post('/api/telebit.cloud/pair_request', function (req, res) { | app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||||
|   var auth = req.body; |   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) { | app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) { | ||||||
|   var secret = req.params.secret; |   var secret = req.params.secret; | ||||||
|   var auth = Auths.getBySecret(secret); |   var auth = Auths.getBySecret(secret); | ||||||
|   var crypto = require('crypto'); |   //var crypto = require('crypto');
 | ||||||
|   var response = {}; |   var response = {}; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										78
									
								
								lib/relay.js
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								lib/relay.js
									
									
									
									
									
								
							| @ -1,10 +1,15 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var url = require('url'); | var url = require('url'); | ||||||
| var PromiseA = require('bluebird'); |  | ||||||
| var sni = require('sni'); | var sni = require('sni'); | ||||||
| var Packer = require('proxy-packer'); | var Packer = require('proxy-packer'); | ||||||
| var PortServers = {}; | var PortServers = {}; | ||||||
|  | var PromiseA; | ||||||
|  | try { | ||||||
|  |   PromiseA = require('bluebird'); | ||||||
|  | } catch(e) { | ||||||
|  |   PromiseA = global.Promise; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| function timeoutPromise(duration) { | function timeoutPromise(duration) { | ||||||
|   return new PromiseA(function (resolve) { |   return new PromiseA(function (resolve) { | ||||||
| @ -240,10 +245,10 @@ var Server = { | |||||||
| 
 | 
 | ||||||
|     return result || srv.socketId; |     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('\n[relay.js] onAuth'); | ||||||
|     console.log(newAuth); |     console.log(rawAuth); | ||||||
|     console.log(grant); |     //console.log(grant);
 | ||||||
|     //var stringauth;
 |     //var stringauth;
 | ||||||
|     var err; |     var err; | ||||||
|     if (!grant || 'object' !== typeof grant) { |     if (!grant || 'object' !== typeof grant) { | ||||||
| @ -253,13 +258,24 @@ var Server = { | |||||||
|       return state.Promise.reject(err); |       return state.Promise.reject(err); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if ('string' !== typeof newAuth) { |     if ('string' !== typeof rawAuth) { | ||||||
|       newAuth = JSON.stringify(newAuth); |       rawAuth = JSON.stringify(rawAuth); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     console.log('check for upgrade token'); |     // TODO don't fire the onAuth event on non-authz updates
 | ||||||
|     if (grant.jwt && newAuth !== grant.jwt) { |     if (!grant.jwt && !(grant.domains||[]).length && !(grant.ports||[]).length) { | ||||||
|       console.log('new token to send back'); |       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
 |         // Access Token
 | ||||||
|         Server.sendTunnelMsg( |         Server.sendTunnelMsg( | ||||||
|           srv |           srv | ||||||
| @ -273,6 +289,7 @@ var Server = { | |||||||
|         // these aren't needed internally once they're sent
 |         // these aren't needed internally once they're sent
 | ||||||
|         grant.jwt = null; |         grant.jwt = null; | ||||||
|       } |       } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /* |     /* | ||||||
|     if (!Array.isArray(grant.domains) || !grant.domains.length) { |     if (!Array.isArray(grant.domains) || !grant.domains.length) { | ||||||
| @ -288,18 +305,20 @@ var Server = { | |||||||
|       return state.Promise.reject(err); |       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
 |     // 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.
 |     // domains and the list of all this websocket's grants.
 | ||||||
|     grant.domains.forEach(function (domainname) { |     grant.domains.forEach(function (domainname) { | ||||||
|       console.log('add', domainname, 'to device lists'); |       console.log('add', domainname, 'to device lists'); | ||||||
|       srv.domainsMap[domainname] = true; |       srv.domainsMap[domainname] = true; | ||||||
|       Devices.add(state.deviceLists, domainname, srv); |       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.domains = Object.keys(srv.domainsMap); | ||||||
|     srv.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || srv.domains.join(','); |     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.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || grant.domains.join(','); | ||||||
|     grant.srv = srv; |     //grant.srv = srv;
 | ||||||
|     //grant.ws = srv.ws;
 |     //grant.ws = srv.ws;
 | ||||||
|     //grant.upgradeReq = srv.upgradeReq;
 |     //grant.upgradeReq = srv.upgradeReq;
 | ||||||
|     grant.clients = {}; |     grant.clients = {}; | ||||||
| @ -344,7 +363,7 @@ var Server = { | |||||||
|     } |     } | ||||||
|     grant.ports.forEach(openPort); |     grant.ports.forEach(openPort); | ||||||
| 
 | 
 | ||||||
|     srv.grants[newAuth] = grant; |     srv.grants[rawAuth] = grant; | ||||||
|     console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc); |     console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc); | ||||||
| 
 | 
 | ||||||
|     console.log('notify of grants', grant.domains, grant.ports); |     console.log('notify of grants', grant.domains, grant.ports); | ||||||
| @ -416,31 +435,35 @@ var Server = { | |||||||
|       process.nextTick(function () { conn.resume(); }); |       process.nextTick(function () { conn.resume(); }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| , addToken: function addToken(state, srv, newAuth) { | , addToken: function addToken(state, srv, rawAuth) { | ||||||
|     console.log("addToken", newAuth); |     console.log("[addToken]", rawAuth); | ||||||
|     if (srv.grants[newAuth]) { |     if (srv.grants[rawAuth]) { | ||||||
|       console.log("addToken - duplicate"); |       console.log("addToken - duplicate"); | ||||||
|       // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 |       // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | ||||||
|       return state.Promise.resolve(null); |       return state.Promise.resolve(null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return state.authenticate({ auth: newAuth }).then(function (authnToken) { |     return state.authenticate({ auth: rawAuth }).then(function (validatedTokenData) { | ||||||
| 
 |       console.log('\n[relay.js] rawAuth'); | ||||||
|       console.log('\n[relay.js] newAuth'); |       console.log(rawAuth); | ||||||
|       console.log(newAuth); |  | ||||||
| 
 | 
 | ||||||
|       console.log('\n[relay.js] authnToken'); |       console.log('\n[relay.js] authnToken'); | ||||||
|       console.log(authnToken); |       console.log(validatedTokenData); | ||||||
| 
 | 
 | ||||||
|       if (authnToken.id) { |       // For tracking state between token exchanges
 | ||||||
|         state.srvs[authnToken.id] = state.srvs[authnToken.id] || {}; |       // and tacking on extra attributes (i.e. for extensions)
 | ||||||
|         state.srvs[authnToken.id].updateAuth = function (validToken) { |       // TODO close on delete
 | ||||||
|           return Server.onAuth(state, srv, newAuth, validToken); |       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); | ||||||
|         }; |         }; | ||||||
|       } |       } | ||||||
| 
 |       state.srvs[validatedTokenData.id].updateAuth(srv, validatedTokenData); | ||||||
|       // will return rejection if necessary
 |  | ||||||
|       return state.srvs[authnToken.id].updateAuth(authnToken); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| , removeToken: function removeToken(state, srv, jwtoken) { | , removeToken: function removeToken(state, srv, jwtoken) { | ||||||
| @ -587,6 +610,7 @@ module.exports.create = function (state) { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (initToken) { |     if (initToken) { | ||||||
|  |       console.log('[wss.onConnection] token provided in http headers'); | ||||||
|       return Server.addToken(state, srv, initToken).then(function () { |       return Server.addToken(state, srv, initToken).then(function () { | ||||||
|         Server.init(state, srv); |         Server.init(state, srv); | ||||||
|       }).catch(function (err) { |       }).catch(function (err) { | ||||||
|  | |||||||
| @ -46,6 +46,12 @@ module.exports.createTcpConnectionHandler = function (state) { | |||||||
|       function tryTls() { |       function tryTls() { | ||||||
|         var vhost; |         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) { |         if (!state.servernames.length) { | ||||||
|           console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)"); |           console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)"); | ||||||
|           deferData('httpsSetupServer'); |           deferData('httpsSetupServer'); | ||||||
| @ -62,12 +68,6 @@ module.exports.createTcpConnectionHandler = function (state) { | |||||||
|           console.log("TODO: use www bare redirect"); |           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() { |         function run() { | ||||||
|           var nextDevice = Devices.next(state.deviceLists, servername); |           var nextDevice = Devices.next(state.deviceLists, servername); | ||||||
|           if (!nextDevice) { |           if (!nextDevice) { | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @ -37,8 +37,7 @@ | |||||||
|   }, |   }, | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js", |   "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@coolaj86/urequest": "^1.1.1", |     "@coolaj86/urequest": "^1.3.2", | ||||||
|     "bluebird": "^3.5.1", |  | ||||||
|     "body-parser": "^1.18.3", |     "body-parser": "^1.18.3", | ||||||
|     "cluster-store": "^2.0.8", |     "cluster-store": "^2.0.8", | ||||||
|     "connect-cors": "^0.5.6", |     "connect-cors": "^0.5.6", | ||||||
| @ -48,16 +47,22 @@ | |||||||
|     "greenlock": "^2.2.4", |     "greenlock": "^2.2.4", | ||||||
|     "human-readable-ids": "^1.0.4", |     "human-readable-ids": "^1.0.4", | ||||||
|     "js-yaml": "^3.11.0", |     "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", |     "nowww": "^1.2.1", | ||||||
|     "proxy-packer": "^1.4.3", |     "proxy-packer": "^1.4.3", | ||||||
|     "recase": "^1.0.4", |     "recase": "^1.0.4", | ||||||
|     "redirect-https": "^1.1.5", |     "redirect-https": "^1.1.5", | ||||||
|     "request": "^2.87.0", |     "request": "^2.87.0", | ||||||
|  |     "safe-replace": "^1.0.3", | ||||||
|     "serve-static": "^1.13.2", |     "serve-static": "^1.13.2", | ||||||
|     "sni": "^1.0.0", |     "sni": "^1.0.0", | ||||||
|     "ws": "^5.1.1" |     "ws": "^5.1.1" | ||||||
|   }, |   }, | ||||||
|  |   "trulyOptionalDependencies": { | ||||||
|  |     "bluebird": "^3.5.1" | ||||||
|  |   }, | ||||||
|   "engineStrict": true, |   "engineStrict": true, | ||||||
|   "engines": { |   "engines": { | ||||||
|     "node": "10.2.1" |     "node": "10.2.1" | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user