UNPKG

@house-agency/brewsession

Version:

The Brewery Session Manager

332 lines (299 loc) 8.13 kB
const _ = require('lodash'); const conf = require('@house-agency/brewtils/config'); const crypto = require('crypto'); const def = require('@house-agency/brewtils/catch-default'); const format = require('util').format; const keyvalue = require('@house-agency/brewstore/keyvalue'); const log = require('@house-agency/brewtils/log'); const q = require('q'); const sha = require('jssha'); /** * Store session oriented data in key/value storage. * It uses storage keys with pattern session:<session-token>:<id> * * @param {string} token Session token * @param {string} id Data identifier key * @param {string} method Method to use for get/set * * @return {object} Promise with db results */ function data(token, id, method) { const args = _.toArray(arguments).slice(3); const key = format('sessions:%s:%s', token, id); return keyvalue.run(key, method, args); } /** * Parses provided api key * * @param {string} key Api key as it's stored in database * * @returns {object} { time: <generation-time>, token: <key-token> } */ function parse_apikey(key) { const arr = key.split(':'); if (arr.length === 2) { return { time: parseInt(arr[0], 10), token: arr[1] }; } throw new Error('Could not parse api key'); } /** * Get current api keys from database * * @return {array} Keys in an array - in a promise */ function get_apikeys() { return keyvalue.run('api-keys', 'lrange', [0, -1]); } /** * Matches provided key against config and keys stored in db * * @param {string} key * * @return {string} key if success - in a promise, throws error on fail */ function match_apikey(key) { return conf('api.key', null) .then(conf_key => { if (conf_key !== null && conf_key === key) { return key; } return get_apikeys() .then(db_keys => { if (_.indexOf(db_keys, key) > -1) { return key; } throw new Error('Invalid key'); }); }); } /** * Removes specified api key from db * * @param {string} key * @return {null} Empty promise */ function remove_apikey(key) { return keyvalue.run('api-keys', 'lrem', [0, key]); } /** * Generates a new api key, stores it in the database * and returns it. * * It will only create one new key since last key generation * plus api.generate time in config. Removes keys older than * api.remove in config. * * Keys are stored in a key array called api-keys with pattern * <generation-time>:<key-token> * * @return {string} Key in a promise */ function generate_apikey() { return q.all([ // Get config data and keys conf('api.generate'), conf('api.remove'), get_apikeys() .then(function (keys) { if (keys.length > 0) { return keys; } throw new Error('Not found'); }) ]) .spread((time_generate, time_remove, keys) => { // Validate keys by dates and remove keys that aren't valid. return _.reduce(keys, (queue, key) => { const time = parse_apikey(key).time; if (time + time_remove < (new Date()).getTime()) { // Not valid - remove return queue .then(() => { return remove_apikey(key); }); } return queue; }, q()) .then(() => { // If last key was generated after api.generate time // has passed, throw not-found error which'll trigger // a new key generation. const key = _.last(keys); const time = parse_apikey(key).time; if (time + time_generate < (new Date()).getTime()) { throw new Error('Not found'); } return key; }); }) .catch(def.not_found(() => { return q.ninvoke(crypto, 'randomBytes', 256) .then(random_buffer => { const sha224 = new sha('SHA-224', 'TEXT'); sha224.update(random_buffer.toString()); return sha224.getHash('HEX'); }) .then(hash => { const key = format( '%d:%s', (new Date()).getTime(), hash ); return keyvalue.run('api-keys', 'rpush', [key]) .then(() => { log('debug', 'Generated api key', key); return key; }); }); })); } /** * Generates a session token by api key * * @param {string} key Api key * * @return {string} Session token in a promise */ function generate_token(key) { return q.all([ q.ninvoke(crypto, 'randomBytes', 256), new sha('SHA-224', 'TEXT') ]) .spread((random_buffer, sha224) => { sha224.update(format( '%s%s%s', key, random_buffer.toString(), (new Date()).getTime() )); return sha224.getHash('HEX'); }); } /** * Get the validation time for session by session token * * @param {string} token Session token * * @return {object} Promise with timestamp in milliseconds and a extras string */ function get_valid(token) { return q.all([ data(token, 'valid', 'get'), data(token, 'extras', 'get') ]) .spread((time, extras) => { return { time: time, extras: extras }; }); } /** * Set the validation time for session by session token * * @param {string} token Session token * @param {number} time Timestamp in milliseconds * * @return {object} Empty promise */ function set_valid(token, time) { return data(token, 'valid', 'set', time); } /** * Creates a session by an API-key and returns a session token * Or fails of key is not valid. * * @param {string} key API-key * @param {...*} args Extra arguments to limit validation: user-agent, ip, whatever * * @return {string} Promise with session token string */ function create(key) { return match_apikey(key) .then(key => { return generate_token(key); }) .then(token => { const sha224 = new sha('SHA-224', 'TEXT'); sha224.update(_.toArray(arguments).slice(1).join('')); return q.all([ set_valid(token, (new Date()).getTime()), data(token, 'extras', 'set', sha224.getHash('HEX')) ]) .then(() => { return token; }); }); } /** * Verifies session by it's token and if it's still valid * * @param {string} token Session token * @param {...*} args Extra arguments to limit validation: user-agent, ip, whatever * * @return {object} Promise with session token string or fail */ function verify(token) { const sha224 = new sha('SHA-224', 'TEXT'); sha224.update(_.toArray(arguments).slice(1).join('')); return q.all([ conf('api.valid'), get_valid(token), sha224.getHash('HEX') ]) .spread((valid, session, extras) => { if ( session.time + parseInt(valid, 10) > (new Date()).getTime() && session.extras === extras ) { return token; } throw new Error('Invalid session token'); }); } /** * Exceeds the expiration time for session by token * * @param {string} token Session token * * @return {object} Promise with session token */ function exceed(token) { return set_valid(token, (new Date()).getTime()) .then(() => { return token; }); } /** * Removes session by token * * @param {string} token Session token * * @return {object} Empty promise */ function remove(token) { return data(token, '*', 'keys') .then(keys => { return q.all(_.map(keys, function (key) { return keyvalue.run(key, 'del'); })); }); } module.exports = _.reduce([ generate_apikey, get_apikeys, match_apikey, remove_apikey, create, verify, exceed, remove, get_valid, data ], function (exp, func) { exp[func.name] = func; return exp; }, {});