| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var PromiseA = require('bluebird'); | 
					
						
							|  |  |  | var path = require('path'); | 
					
						
							|  |  |  | var fs = PromiseA.promisifyAll(require('fs')); | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  | var jwt = require('jsonwebtoken'); | 
					
						
							|  |  |  | var crypto = require('crypto'); | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | module.exports.create = function (deps, conf) { | 
					
						
							| 
									
										
										
										
											2017-07-07 17:53:12 -06:00
										 |  |  |   var hrIds = require('human-readable-ids').humanReadableIds; | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |   var scmp = require('scmp'); | 
					
						
							|  |  |  |   var storageDir = path.join(__dirname, '..', 'var'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function read(fileName) { | 
					
						
							|  |  |  |     return fs.readFileAsync(path.join(storageDir, fileName)) | 
					
						
							|  |  |  |     .then(JSON.parse, function (err) { | 
					
						
							| 
									
										
										
										
											2017-07-07 13:48:40 -06:00
										 |  |  |       if (err.code === 'ENOENT') { | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |         return {}; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       throw err; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   function write(fileName, obj) { | 
					
						
							|  |  |  |     return fs.mkdirAsync(storageDir).catch(function (err) { | 
					
						
							|  |  |  |       if (err.code !== 'EEXIST') { | 
					
						
							|  |  |  |         console.error('failed to mkdir', storageDir, err.toString()); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }).then(function () { | 
					
						
							|  |  |  |       return fs.writeFileAsync(path.join(storageDir, fileName), JSON.stringify(obj), 'utf8'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   var owners = { | 
					
						
							|  |  |  |     _filename: 'owners.json' | 
					
						
							|  |  |  |   , all: function () { | 
					
						
							|  |  |  |       return read(this._filename).then(function (owners) { | 
					
						
							|  |  |  |         return Object.keys(owners).map(function (id) { | 
					
						
							|  |  |  |           var owner = owners[id]; | 
					
						
							|  |  |  |           owner.id = id; | 
					
						
							|  |  |  |           return owner; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , get: function (id) { | 
					
						
							|  |  |  |       // While we could directly read the owners file and access the id directly from
 | 
					
						
							|  |  |  |       // the resulting object I'm not sure of the details of how the object key lookup
 | 
					
						
							|  |  |  |       // works or whether that would expose us to timing attacks.
 | 
					
						
							|  |  |  |       // See https://codahale.com/a-lesson-in-timing-attacks/
 | 
					
						
							|  |  |  |       return this.all().then(function (owners) { | 
					
						
							|  |  |  |         return owners.filter(function (owner) { | 
					
						
							|  |  |  |           return scmp(id, owner.id); | 
					
						
							|  |  |  |         })[0]; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , exists: function (id) { | 
					
						
							|  |  |  |       return this.get(id).then(function (owner) { | 
					
						
							|  |  |  |         return !!owner; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , set: function (id, obj) { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							|  |  |  |       return read(self._filename).then(function (owners) { | 
					
						
							|  |  |  |         obj.id = id; | 
					
						
							|  |  |  |         owners[id] = obj; | 
					
						
							|  |  |  |         return write(self._filename, owners); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-10 11:08:19 -06:00
										 |  |  |   var confCb; | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |   var config = { | 
					
						
							|  |  |  |     save: function (changes) { | 
					
						
							|  |  |  |       deps.messenger.send({ | 
					
						
							| 
									
										
										
										
											2017-07-06 13:09:20 -06:00
										 |  |  |         type: 'com.daplie.goldilocks/config' | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |       , changes: changes | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-10-10 11:08:19 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |       return new deps.PromiseA(function (resolve, reject) { | 
					
						
							|  |  |  |         var timeoutId = setTimeout(function () { | 
					
						
							|  |  |  |           reject(new Error('Did not receive config update from main process in a reasonable time')); | 
					
						
							|  |  |  |           confCb = null; | 
					
						
							|  |  |  |         }, 15*1000); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         confCb = function (config) { | 
					
						
							|  |  |  |           confCb = null; | 
					
						
							|  |  |  |           clearTimeout(timeoutId); | 
					
						
							|  |  |  |           resolve(config); | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2017-10-10 11:08:19 -06:00
										 |  |  |   function updateConf(config) { | 
					
						
							|  |  |  |     if (confCb) { | 
					
						
							|  |  |  |       confCb(config); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |   var userTokens = { | 
					
						
							|  |  |  |     _filename: 'user-tokens.json' | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |   , _cache: {} | 
					
						
							|  |  |  |   , _convertToken: function convertToken(id, token) { | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |       // convert the token into something that looks more like what OAuth3 uses internally
 | 
					
						
							|  |  |  |       // as sessions so we can use it with OAuth3. We don't use OAuth3's internal session
 | 
					
						
							|  |  |  |       // storage because it effectively only supports storing tokens based on provider URI.
 | 
					
						
							|  |  |  |       // We also use the token as the `access_token` instead of `refresh_token` because the
 | 
					
						
							|  |  |  |       // refresh functionality is closely tied to the storage.
 | 
					
						
							|  |  |  |       var decoded = jwt.decode(token); | 
					
						
							| 
									
										
										
										
											2017-10-18 15:37:35 -06:00
										 |  |  |       if (!decoded) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |       return { | 
					
						
							|  |  |  |         id:           id | 
					
						
							|  |  |  |       , access_token: token | 
					
						
							|  |  |  |       , token:        decoded | 
					
						
							|  |  |  |       , provider_uri: decoded.iss || decoded.issuer || decoded.provider_uri | 
					
						
							|  |  |  |       , client_uri:   decoded.azp | 
					
						
							|  |  |  |       , scope:        decoded.scp || decoded.scope || decoded.grants | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , all: function allUserTokens() { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |       if (self._cacheComplete) { | 
					
						
							|  |  |  |         return deps.PromiseA.resolve(Object.values(self._cache)); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |       return read(self._filename).then(function (tokens) { | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |         // We will read every single token into our cache, so it will be complete once we finish
 | 
					
						
							|  |  |  |         // creating the result (it's set out of order so we can directly return the result).
 | 
					
						
							|  |  |  |         self._cacheComplete = true; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |         return Object.keys(tokens).map(function (id) { | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |           self._cache[id] = self._convertToken(id, tokens[id]); | 
					
						
							|  |  |  |           return self._cache[id]; | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , get: function getUserToken(id) { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |       if (self._cache.hasOwnProperty(id) || self._cacheComplete) { | 
					
						
							|  |  |  |         return deps.PromiseA.resolve(self._cache[id] || null); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |       return read(self._filename).then(function (tokens) { | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |         self._cache[id] = self._convertToken(id, tokens[id]); | 
					
						
							|  |  |  |         return self._cache[id]; | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , save: function saveUserToken(newToken) { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							|  |  |  |       return read(self._filename).then(function (tokens) { | 
					
						
							|  |  |  |         var rawToken; | 
					
						
							|  |  |  |         if (typeof newToken === 'string') { | 
					
						
							|  |  |  |           rawToken = newToken; | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           rawToken = newToken.refresh_token || newToken.access_token; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (typeof rawToken !== 'string') { | 
					
						
							|  |  |  |           throw new Error('cannot save invalid session: missing refresh_token and access_token'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var decoded = jwt.decode(rawToken); | 
					
						
							|  |  |  |         var idHash = crypto.createHash('sha256'); | 
					
						
							|  |  |  |         idHash.update(decoded.sub || decoded.ppid || decoded.appScopedId || ''); | 
					
						
							|  |  |  |         idHash.update(decoded.iss || decoded.issuer || ''); | 
					
						
							|  |  |  |         idHash.update(decoded.aud || decoded.audience || ''); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var scope = decoded.scope || decoded.scp || decoded.grants || ''; | 
					
						
							|  |  |  |         idHash.update(scope.split(/[,\s]+/mg).sort().join(',')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var id = idHash.digest('hex'); | 
					
						
							|  |  |  |         tokens[id] = rawToken; | 
					
						
							|  |  |  |         return write(self._filename, tokens).then(function () { | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |           // Delete the current cache so that if this is an update it will refresh
 | 
					
						
							|  |  |  |           // the cache once we read the ID.
 | 
					
						
							|  |  |  |           delete self._cache[id]; | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |           return self.get(id); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , remove: function removeUserToken(id) { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							|  |  |  |       return read(self._filename).then(function (tokens) { | 
					
						
							|  |  |  |         var present = delete tokens[id]; | 
					
						
							|  |  |  |         if (!present) { | 
					
						
							|  |  |  |           return present; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return write(self._filename, tokens).then(function () { | 
					
						
							| 
									
										
										
										
											2017-10-19 12:58:04 -06:00
										 |  |  |           delete self._cache[id]; | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |           return true; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-07 17:53:12 -06:00
										 |  |  |   var mdnsId = { | 
					
						
							|  |  |  |     _filename: 'mdns-id' | 
					
						
							|  |  |  |   , get: function () { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							|  |  |  |       return read("mdns-id").then(function (result) { | 
					
						
							|  |  |  |         if (typeof result !== 'string') { | 
					
						
							|  |  |  |           throw new Error('mDNS ID not present'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return result; | 
					
						
							|  |  |  |       }).catch(function () { | 
					
						
							|  |  |  |         return self.set(hrIds.random()); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   , set: function (value) { | 
					
						
							|  |  |  |       var self = this; | 
					
						
							|  |  |  |       return write(self._filename, value).then(function () { | 
					
						
							|  |  |  |         return self.get(); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |   return { | 
					
						
							|  |  |  |     owners: owners | 
					
						
							|  |  |  |   , config: config | 
					
						
							| 
									
										
										
										
											2017-10-10 11:08:19 -06:00
										 |  |  |   , updateConf: updateConf | 
					
						
							| 
									
										
										
										
											2017-10-17 18:36:36 -06:00
										 |  |  |   , tokens: userTokens | 
					
						
							| 
									
										
										
										
											2017-07-07 17:53:12 -06:00
										 |  |  |   , mdnsId: mdnsId | 
					
						
							| 
									
										
										
										
											2017-07-06 11:01:29 -06:00
										 |  |  |   }; | 
					
						
							|  |  |  | }; |