204 lines
6.0 KiB
JavaScript
204 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
var PromiseA = require('bluebird');
|
|
var path = require('path');
|
|
var fs = PromiseA.promisifyAll(require('fs'));
|
|
var jwt = require('jsonwebtoken');
|
|
var crypto = require('crypto');
|
|
|
|
module.exports.create = function (deps, conf) {
|
|
var hrIds = require('human-readable-ids').humanReadableIds;
|
|
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) {
|
|
if (err.code === 'ENOENT') {
|
|
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);
|
|
});
|
|
}
|
|
};
|
|
|
|
var confCb;
|
|
var config = {
|
|
save: function (changes) {
|
|
deps.messenger.send({
|
|
type: 'com.daplie.goldilocks/config'
|
|
, changes: changes
|
|
});
|
|
|
|
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);
|
|
};
|
|
});
|
|
}
|
|
};
|
|
function updateConf(config) {
|
|
if (confCb) {
|
|
confCb(config);
|
|
}
|
|
}
|
|
|
|
var userTokens = {
|
|
_filename: 'user-tokens.json'
|
|
, _convertToken(id, token) {
|
|
// 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);
|
|
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;
|
|
return read(self._filename).then(function (tokens) {
|
|
return Object.keys(tokens).map(function (id) {
|
|
return self._convertToken(id, tokens[id]);
|
|
});
|
|
});
|
|
}
|
|
, get: function getUserToken(id) {
|
|
var self = this;
|
|
return read(self._filename).then(function (tokens) {
|
|
return self._convertToken(id, tokens[id]);
|
|
});
|
|
}
|
|
, 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 () {
|
|
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 () {
|
|
return true;
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
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();
|
|
});
|
|
}
|
|
};
|
|
|
|
return {
|
|
owners: owners
|
|
, config: config
|
|
, updateConf: updateConf
|
|
, tokens: userTokens
|
|
, mdnsId: mdnsId
|
|
};
|
|
};
|