UNPKG

bip-pod

Version:
1,956 lines (1,619 loc) 65.8 kB
/** * * The Bipio Pod Bridge. Provides basic system resources, auth helpers, * setup, invoke and data sources for actions within the pod. * * Copyright (c) 2017 InterDigital, Inc. All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var cron = require('cron'), crypto = require('crypto'), dns = require('dns'), extend = require('extend'); fs = require('fs'), ipaddr = require('ipaddr.js'), JSONPath = require('JSONPath'), mime = require('mime'), moment = require('moment'), passport = require('passport'), request = require('request'), tldtools = require('tldtools'), _ = require('underscore'), util = require('util'), uuid = require('node-uuid'), validator = require('validator'); // utility resources var helper = { isObject : function(obj) { return Object.prototype.toString.call(obj) == "[object Object]"; }, toUTC: function(date) { return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); }, now: function() { return new Date(); }, nowUTC: function() { return helper.toUTC(this.now()); }, nowUTCMS: function() { return helper.nowUTC().getTime(); }, nowUTCSeconds: function() { return helper.nowUTCMS() / 1000; }, // Returns all ipv4/6 A records for a host resolveHost : function(host, next) { var tokens = tldtools.extract(host), resolvingHost; if (ipaddr.IPv4.isValid(host) || ipaddr.IPv6.isValid(host) ) { next(false, [ host ], host); } else { resolvingHost = tokens.inspect.getDomain() || tokens.domain; dns.resolve(resolvingHost, function(err, aRecords) { next(err, aRecords, resolvingHost ); }); } }, JSONPath : function(obj, path) { return JSONPath.eval(obj, path); }, getObject : function(input) { if (!helper.isObject(input)) { input = JSON.parse(input); } return input; }, isObject: function(src) { return (helper.getType(src) == '[object Object]'); }, isNumber: function(src) { return (helper.getType(src) == '[object Number]'); }, isBoolean: function(src){ return (src === true || src === false || 1 === src || 0 === src || (helper.isString(src) && ['true', 'false', '1', '0','yes','no','y','n'].indexOf(src.toLowerCase()) >= 0)); }, isNumeric : function(src){ return validator.validators.isNumeric(src); }, isFloat : function(src){ return validator.validators.isFloat(src); }, isInt : function(src) { if (this.isNumber(src)) { return src === +src && src === (src|0); } else { return validator.validators.isInt(src); } }, isArray: function(src) { return (helper.getType(src) == '[object Array]'); }, isJson: function(src) { return this.isArray(src) || this.isObject(src); }, isString : function(src) { return (helper.getType(src) == '[object String]'); }, isFunction : function(src) { return (helper.getType(src) == '[object Function]'); }, isTruthy : function(src) { return (src === true || 1 === src || (helper.isString(src) && ['true', '1','yes','y'].indexOf(src.toLowerCase()) >= 0)) }, isFalsy : function(src) { return (src === false || 0 === src || (helper.isString(src) && ['false', '0','no','n'].indexOf(src.toLowerCase()) >= 0)) }, stringToFloat : function(str){ if(helper.isFloat(str)){ return parseFloat(str); }else{ return false; } }, stringToInt : function(str) { if(helper.isInt(str)){ return parseInt(str); }else{ return false; } }, stringToJson: function(str) { try { var parsed = JSON.parse(str); if(helper.isJson(parsed)){ return parsed; }else{ return null; } } catch (e) { return null; } }, stringToArray: function(str){ try { var parsed = JSON.parse(str); if(helper.isArray(parsed)){ return parsed; }else{ return null; } } catch (e) { return null; } }, string_isArray : function(src){ try { return helper.isArray(JSON.parse(src)); } catch (e) { return false; } }, getType: function(src) { return Object.prototype.toString.call( src ); }, stringToBoolean:function(src) { return (helper.isTruthy(src) || helper.isFalsy(src)) }, sanitize : function(str) { return validator.sanitize(str); }, scrub: function(str, noEscape) { var retStr = helper.sanitize(str).xss(); retStr = helper.sanitize(retStr).trim(); return retStr; }, /** * Cleans an object thoroughly. Script scrubbed, html encoded. */ pasteurize: function(src, noEscape) { var attrLen, newKey; if (helper.isArray(src)) { var attrLen = src.length; for (var i = 0; i < attrLen; i++) { src[i] = helper.pasteurize(src[i], noEscape); } } else if (this.isString(src)) { src = helper.scrub(src, noEscape); } else if (helper.isObject(src)) { var newSrc = {}; for (key in src) { newKey = helper.scrub(key); newSrc[newKey] = helper.pasteurize(src[key], noEscape); } src = newSrc; } return src; }, naturalize : function(src) { var attrLen, newKey; if (helper.isArray(src)) { var attrLen = src.length; for (var i = 0; i < attrLen; i++) { src[i] = helper.naturalize(src[i]); } } else if (helper.isString(src)) { src = validator.sanitize(src).entityDecode(); } else if (helper.isObject(src)) { var newSrc = {}; for (key in src) { newKey = validator.sanitize(key).entityDecode(); newSrc[newKey] = helper.naturalize(src[key]); } src = newSrc; } return src; }, strHash : function(str) { return crypto.createHash('md5').update(str.toLowerCase()).digest("hex"); }, // Stream helpers streamToHash : function(readStream, next) { var hash = crypto.createHash('sha1'); hash.setEncoding('hex'); readStream.on('end', function() { hash.end(); next(false, hash.read()); }); readStream.on('error', function(err) { next(err); }); readStream.pipe(hash); }, streamToBuffer : function(readStream, next) { var buffers = []; readStream.on('data', function(chunk) { buffers.push(chunk); }); readStream.on('error', function(err) { next(err); }); readStream.on('end', function() { next(false, Buffer.concat(buffers)); }); } } // pod required fields var requiredMeta = [ 'name', 'title', 'description' ]; // constructor function Pod(metadata, init) { metadata = metadata || {}; // oauth provider token refresh method // @todo deprecate for an implementation of oAuthRefresh in pod if (metadata.oAuthRefresh) { this._oAuthRefresh = metadata.oAuthRefresh; } // post-constructor this._podInit = init; // Bip Pod Manifest this._bpm = {}; // pod resources bridge this.$resource = {}; // DAO this._dao = null; // logger this._logger = null; // action prototypes this._actionProtos = []; // action instances this.actions = {}; // crons this.crons = {}; // options this.options = { baseURL : '', blacklist : [], timezone : 'UTC', cdnPublicBaseURL : '', emitterBaseURL : '', cdnBasePath : '', config : {} }; this._oAuthRegistered = false; } Pod.prototype = { getPodBase : function(podName, literal) { return __dirname + (literal ? '/' : '/../bip-pod-') + podName; }, /** * make system resources available to the pod. Invoked by the Pod registrar * in the Channels model when bootstrapping. * * @param dao {Object} DAO. * @param config {Object} Pod Config * @param options {Object} system options * */ init : function(podName, dao, cdn, logger, options) { var reqBase = this.getPodBase(podName, options.reqLiteral), self = this; this.setSchema(require(reqBase + '/manifest.json')); // check required meta's for (var i = 0; i < requiredMeta.length; i++) { if (!this.getBPMAttr(requiredMeta[i])) { throw new Error(podName + ' Pod is missing required "' + requiredMeta[i] + '" metadata'); } } var dataSources = this.getDataSources(), self = this, dataSource, model; // set stored config if (options.config) { this.setConfig(options.config); } // merge options _.each(options, function(value, key) { if ('config' !== key) { self.options[key] = value; } }); if (dao) { this._dao = dao; } if (logger) { this._logger = logger; } if (cdn) { this.cdn = cdn; } if (this._dao) { // register generic tracker var tracker = require('./models/channel_pod_tracking'); this._dao.registerModel(tracker); // create pod tracking container for duplicate entities if (this.getTrackDuplicates()) { var podDupTracker = _.clone(require('./models/dup')); podDupTracker.entityName = this.getDataSourceName(podDupTracker.entityName); this._dao.registerModel(podDupTracker); } // create pod tracking container for duplicate entities if (this.getTrackDeltas()) { var podDeltaTracker = _.clone(require('./models/delta')); podDeltaTracker.entityName = this.getDataSourceName(podDeltaTracker.entityName); this._dao.registerModel(podDeltaTracker); } // register pod data sources for (var dsName in dataSources) { if (dataSources.hasOwnProperty(dsName)) { dataSource = _.clone(dataSources[dsName]); // namespace the model + create an internal representation dataSource.entityName = this.getDataSourceName(dsName); dataSource.entitySchema = dataSource.properties; dataSource.compoundKeyConstraints = _.object( _.map( dataSource.keys, function(x) { return [x, 1] } ) ); this._dao.registerModel(dataSource); } } } // register the oauth strategy if ((this.getAuthType() === 'oauth') && (options.config && options.config.oauth)) { var auth = self.getAuth(), pProvider = (auth.passport && auth.passport.provider) ? auth.passport.provider : this.getName(), pStrategy = (auth.passport && auth.passport.strategy) ? auth.passport.strategy : 'Strategy', passport = require(reqBase + '/node_modules/passport-' + pProvider); this._oAuthRegisterStrategy( passport[pStrategy], self.getConfig().oauth ); // cleanup delete auth.passport; } // bind pod renderers var rpcs = this.getRPCs(); _.each(rpcs, function(rpc, key) { rpc._href = self.options.baseUrl + '/rpc/pod/' + self.getName() + '/render/' + key; if (!rpc.method) { rpc.method = 'GET'; } if (!rpc.name) { rpc.name = key; } }); // // --- CREATE RESOURCES // // create resources for Actions this.$resource.dao = dao; this.$resource.moment = moment; this.$resource.mime = mime; this.$resource.uuid = uuid; this.$resource.tldtools = tldtools; this.$resource.sanitize = validator.sanitize; this.$resource._ = _; this.$resource.accumulateFilter = this.accumulateFilter; this.$resource.dupFilter = this.dupFilter; this.$resource.deltaFilter = this.deltaFilter; this.$resource.options = self.options; this.$resource.log = (function(scope) { return function() { scope.log.apply(scope, arguments); } })(this); this.$resource.getDataSourceName = function(dsName) { return 'pod_' + self.getName().replace(/-/g, '_') + '_' + dsName; }; this.$resource.getDataDir = this.getDataDir; this.$resource.getCDNDir = this.getCDNDir; this.$resource.expireCDNDir = this.expireCDNDir; this.$resource.getCDNURL = this.getCDNURL; this.$resource._httpGet = this._httpGet; this.$resource._httpPost = this._httpPost; this.$resource._httpPut = this._httpPut; this.$resource._httpStreamToFile = this._httpStreamToFile; this.$resource.helper = helper; this.$resource.stream = { toHash : helper.streamToHash, toBuffer : helper.streamToBuffer } // temporary file management bridge this.$resource.file = cdn; /* this.$resource.file = { get : this._cdnFileGet } */ this.$resource._isVisibleHost = this._isVisibleHost; // give the pod a scheduler if (options.isMaster) { this.$resource.cron = cron; } // --------- BIND ACTIONS // bind actions var action; _.each(this.getActionSchemas(), function(schema, actionName) { if (!schema.disabled) { var reqBase = self.getPodBase(podName, options.reqLiteral), actionProto = require(reqBase + '/' + actionName + '.js'); action = new actionProto(self.getConfig(), self); action.$resource = self.$resource; // bind meta info action.name = actionName; action.schema = schema; action.pod = self; // add to action collection self.actions[actionName] = action; } else { // drop disabled schemas delete self.getActionSchemas()[actionName]; } }); this._limiters = { maxRate : this.getRateLimit(), owners : {} }; if (this._podInit) { this._podInit.apply(this); } }, // tests whether host is in blacklist hostBlacklisted : function(host, whitelist, next) { var blacklist = this.options.blacklist; helper.resolveHost(host, function(err, aRecords, resolvedHost) { var inBlacklist = false; if (!err) { if (whitelist) { if (_.intersection(aRecords, whitelist).length ) { next(err, [], aRecords); return; } else { for (var i = 0; i < whitelist.length; i++) { if (resolvedHost === whitelist[i]) { next(err, [], aRecords); return; } } } } inBlacklist = _.intersection(aRecords, blacklist) } next(err, inBlacklist, aRecords); }); }, _isVisibleHost : function(host, next, channel, whitelist) { var self = this; self.hostBlacklisted(host, whitelist, function(err, blacklisted, resolved) { if (err) { next(err); if (channel) { self.log(err, channel, 'error'); } else { self._logger.call(self, err, 'error'); } } else { next(err, blacklisted, resolved); } }); }, /** * Retrieves matching elements from the manfiest with a JSON Path * When no element found, returns null * * @param string path JSONPath * @returns mixed result or null */ _attrCache : {}, getBPMAttr : function (path) { var val; if (true || !this._attrCache[path]) { var result = helper.JSONPath(this._bpm, path); if (result.length === 1) { val = result[0]; } else if (result.length) { val = result; } else { val = null; } this._attrCache[path] = val; } return this._attrCache[path]; }, getSchema : function() { return this._bpm; }, setSchema : function(bpmJSON) { this._bpm = bpmJSON; }, // --------------------------- BPM path accessors getName : function() { return this.getBPMAttr('name'); }, getTitle : function() { return this.getBPMAttr('title'); }, getDescription : function() { return this.getBPMAttr('description'); }, getIcon : function() { return this.options.cdnPublicBaseURL + '/img/pods/' + this.getName() + '.png'; }, getRateLimit : function() { return this.getBPMAttr('rateLimit'); }, getRPCs : function(rpc) { return this.getBPMAttr('rpcs' + (rpc ? ('.' + rpc) : '' )) || {}; }, getTrackDuplicates : function() { return this.getBPMAttr('trackDuplicates') || false; }, getTrackDeltas : function() { return this.getBPMAttr('trackDeltas') || false; }, getTags : function() { return this.getBPMAttr('tags'); }, // AUTH getAuthType : function() { return this.getBPMAttr('auth.strategy') || 'none'; }, getAuthProperties : function() { return this.getBPMAttr('auth.properties') || {}; }, getAuthDisposition : function() { return this.getBPMAttr('auth.disposition') || []; }, getAuth : function() { var auth = this.getBPMAttr('auth'); auth.status = 'none' === auth.strategy ? 'accepted' : 'required' return auth; }, // POD CONFIG getConfig : function() { return this.getBPMAttr('config') || {}; }, setConfig: function(config) { this._bpm.config = config; }, // DATASOURCES getDataSources : function() { return this.getBPMAttr('dataSources') || {}; }, getDataSourceName : function(dsName) { return 'pod_' + this.getName().replace(/-/g, '_') + '_' + dsName; }, // DAO setDao: function(dao) { this._dao = dao; }, getDao: function() { return this._dao; }, // --------------------------- BPM ACTION Path Accessors getTriggerType : function(action) { return this.getBPMAttr('actions.' + action + '.trigger'); }, getActionSchemas : function() { return this.getBPMAttr('actions'); }, getAction : function(action) { return this.getBPMAttr('actions.' + action); }, getActionConfig : function(action) { return this.getBPMAttr('actions.' + action + '.config'); }, getActionExports : function(action) { return this.getBPMAttr('actions.' + action + '.exports'); }, getActionImports : function(action) { return this.getBPMAttr('actions.' + action + '.imports'); }, getActionRPCs : function(action, rpc) { return this.getBPMAttr('actions.' + action + '.rpcs' + (rpc ? ('.' + rpc) : '')); }, getActionConfigDefaults : function(action) { var defaults = {}, config = this.getActionConfig(action); _.each(config.properties, function(attr, key) { if (attr['default']) { defaults[key] = attr['default']; } }); return defaults; }, getActionImportDefaults : function(action) { var defaults = {}, imports = this.getActionImports(action); _.each(imports.properties, function(attr, key) { if (attr['default']) { defaults[key] = attr['default']; } }); return defaults; }, getActionRPC : function(action, rpc) { return this.getBPMAttr('actions.' + action + '.rpcs.' + rpc); }, // description of the action getActionDescription : function(action) { return this.getAction(action).description; }, // alias for getActionDescription repr : function() { return this.getActionDescription.apply(this, arguments); }, // --------------------------- Compound tests and helpers // invoker for this action can generate its own content (periodically) isTrigger: function(action) { var tt = this.getTriggerType(action); return ('poll' === tt || 'realtime' === tt); }, isRealtime : function(action) { var tt = this.getTriggerType(action); return ('realtime' === tt); }, // action can render its own stored content canRender: function(action) { return this.getBPMAttr('actions.' + action + '.rpcs') !== null; }, // tests whether renderer is available for an action isRenderer : function(action, renderer) { return 'invoke' === renderer || this.getBPMAttr('actions.' + action + '.rpcs.' + renderer) !== null; }, testImport : function(action, importName) { return this.getBPMAttr('actions.' + action + '.imports.' + importName) }, listActions : function() { return _.where(this.getActionSchemas(), { trigger : 'invoke'} ); }, listEmitters : function() { // return this.getBPMAttr('.actions[?(@.trigger!="invoke")]'); return _.filter(this.getActionSchemas(), function(action, key) { return (action.trigger !== 'invoke'); }); }, // provide a scheduler service registerCron : function(id, period, callback) { var self = this; if (this.$resource.cron) { if (!this.crons[id]) { self._logger.call(self, 'POD:Registering Cron:' + self.getName() + ':' + id); self.crons[id] = new self.$resource.cron.CronJob( period, callback, null, true, self.options.timezone ); } } }, // limit the rate at which a fn call can be made. _ratePopper : null, limitRate : function(channel, fn, rateOverride) { var queue, limiters = this._limiters.owners, rateOverride = rateOverride || this._limiters.maxRate; if (!limiters[channel.owner_id]) { limiters[channel.owner_id] = { queue : [] } } limiters[channel.owner_id].queue.push(fn); if (!limiters[channel.owner_id].popper) { limiters[channel.owner_id].popper = setInterval(function() { if (limiters[channel.owner_id].queue.length) { limiters[channel.owner_id].queue.shift()(); } else { clearInterval(limiters[channel.owner_id].popper); delete limiters[channel.owner_id] } }, 1000 / rateOverride); } }, /** * Logs a message */ log : function(message, channel, level) { if (helper.isObject(message)) { this._logger.call(this, channel.action + ':' + (channel.owner_id ? channel.owner_id : 'system'), level); if (message.message) { message = message; } this._logger.call(this, message, level); } else { this._logger.call(this, channel.action + ':' + (channel.owner_id ? channel.owner_id : 'system') + ':' + message, level); } }, // ------------------------------ 3RD PARTY AUTHENTICATION HELPERS testCredentials : function(struct, next) { next(false); }, removeCredential : function(ownerId, providerName, next) { var filter = { owner_id : ownerId }; if (this.isOAuth()) { filter.oauth_provider = providerName; } else { filter.auth_provider = providerName; } this._dao.removeFilter('account_auth', filter, function(err) { if (next) { next(err); } }); }, issuerTokenRPC : function(method, req, res) { var ok = false, accountId = req.remoteUser.getId(); self = this; res.contentType(DEFS.CONTENTTYPE_JSON); if (this.getAuthType() == 'issuer_token') { if (method == 'set') { self._logger.call(self, '[' + accountId + '] ISSUER_TOKEN ' + this.getName() + ' SET' ); // upsert oAuth document var filter = { owner_id : accountId, type : this.getAuthType(), auth_provider : this.getName() }; var struct = { owner_id : accountId, username : req.query.username, key : req.query.key, password : req.query.password, type : this.getAuthType(), auth_provider : this.getName() }; self.testCredentials(struct, function(err, status) { if (err) { res.status(status || 401).jsonp({ "message" : err.toString() }); } else { // @todo upserts don't work with mongoose middleware // create a dao helper for filter -> model upsert. self._dao.find('account_auth', filter, function(err, result) { if (err) { self._logger.call(self, err, 'error'); res.send(500); } else { // update if (result) { self._dao.update('account_auth', result.id, struct, function(err, result) { if (err) { self._logger.call(self, err, 'error'); res.status(500).jsonp({}); } else { res.status(200).jsonp({}); } }, req.remoteUser); } else { // create var model = self._dao.modelFactory('account_auth', struct); self._dao.create(model, function(err, result) { if (err) { self._logger.call(self, err, 'error'); res.status(500).jsonp({}); } else { res.status(200).jsonp({}); } }, req.remoteUser); } } }); } }); ok = true; } else if (method == 'deauth') { this.removeCredential( accountId, this.getName(), function(err) { if (!err) { res.status(200).jsonp({}); } else { self._logger.call(self, err, 'error'); res.status(500).jsonp({}); } } ); ok = true; } } return ok; }, /** * @param string method auth rpc method name * @param object req request * @param object res response */ oAuthRPC: function(method, req, res) { var ok = false, authMethod = (this._oAuthMethod) ? this._oAuthMethod : 'authorize', self = this, podName = this.getName(), accountInfo = req.remoteUser, accountId = accountInfo.getId(), emitterHost = this.options.emitterBaseURL; if (false !== this._oAuthRegistered) { // invoke the passport oauth handler if (method == 'auth') { self._logger.call(self, '[' + accountId + '] OAUTH ' + podName + ' AUTH REQUEST' ); passport[authMethod](this.getName(), this._oAuthConfig)(req, res); ok = true; } else if (method == 'cb') { self._logger.call(self, '[' + accountId + '] OAUTH ' + podName + ' AUTH CALLBACK ' + authMethod ); passport[authMethod](this.getName(), function(err, user) { // @todo - decouple from site. if (err) { self._logger.call(self, err, 'error'); res.redirect(emitterHost + '/oauthcb?status=denied&provider=' + podName); } else if (!user && req.query.error_reason && req.query.error_reason == 'user_denied') { self._logger.call(self, '[' + accountId + '] OAUTH ' + podName + ' CANCELLED' ); res.redirect(emitterHost + '/oauthcb?status=denied&provider=' + podName); } else if (!user) { self._logger.call(self, '[' + accountId + '] OAUTH ' + podName + ' UNKNOWN ERROR' ); res.redirect(emitterHost + '/oauthcb?status=denied&provider=' + podName); } else { self._logger.call(self, '[' + accountId + '] OAUTH ' + podName + ' AUTHORIZED' ); // install singletons // self.autoInstall(accountInfo); res.redirect(emitterHost + '/oauthcb?status=accepted&provider=' + podName); } })(req, res, function(err) { res.send(500); self._logger.call(self, err, 'error'); }); ok = true; } else if (method == 'deauth') { this.oAuthUnbind(accountId, function(err) { if (!err) { res.status(200).end(); } else { self._logger.call(self, err, 'error'); res.status(500).end(); } }); ok = true; // returns 200 OK or 401 Not Authorized } else if (method == 'authstat') { ok = true; } else if (method == 'denied') { res.sendStatus(401); } } return ok; }, authStatus : function(owner_id, next) { if (this.isOAuth()) { this.oAuthStatus(owner_id, next); } else { this._getPassword(owner_id, next); } }, isOAuth : function() { return 'oauth' === this.getAuthType(); }, isIssuerAuth : function() { return 'issuer_token' === this.getAuthType(); }, _getPassword : function(ownerId, next) { var self = this, podName = this.getName(), filter = { owner_id : ownerId, type : this.getAuthType(), auth_provider : podName }; this._dao.find('account_auth', filter, function(err, result) { if (!result || err) { if (err) { self._logger.call(self, err, 'error'); next(true, podName, self.getAuthType(), result ); } else { next(false, podName, self.getAuthType(), result ); } } else { next(false, podName, self.getAuthType(), result); } }); }, // return oAuth profile representation profileReprOAuth : function(profile) { return ''; }, // return issuer token representation profileReprIssuer : function(authModel) { return ''; }, _profileRepr : function(authModel) { var model = this._dao.modelFactory('account_auth', authModel), profile; if (this.isOAuth()) { profile = model.getOauthProfile(); return this.profileReprOAuth(JSON.parse(profile)); } else { return this.profileReprIssuer(model); } }, /** * passes oAuth result set if one exists */ oAuthStatus : function(owner_id, next) { var self = this, podName = this.getName(), filter = { owner_id : owner_id, type : this.getAuthType(), oauth_provider : this.getName() }; this._dao.find('account_auth', filter, function(err, result) { var model, authStruct; if (result) { // normalize oauth representation authStruct = { oauth_provider : result.oauth_provider, repr : self._profileRepr(result) } } next(err, podName, filter.type, authStruct); }); }, /** * * Registers an oAuth strategy for this pod * */ _oAuthRegisterStrategy : function(strategy, config) { var self = this, localConfig = { callbackURL : self.options.baseUrl + '/rpc/oauth/' + this.getName() + '/cb', failureRedirect : self.options.baseUrl + '/rpc/oauth/' + this.getName() + '/denied', passReqToCallback: true }, passportStrategy; for (key in config) { localConfig[key] = config[key]; } self._oAuthConfig = { scope : config.scopes }; if (config.extras) { _.each(config.extras, function(val, key) { self._oAuthConfig[key] = val; }); } this._oAuthRegistered = true; passportStrategy = new strategy( localConfig, function(req, accessToken, refreshToken, params, profile, done) { // maintain scope self.oAuthBinder(req, accessToken, refreshToken, params, profile, done); }); // set strategy name as the pod name // this is for authing separate applications/pods // with the same strategy passportStrategy.name = this.getName(); passport.use(passportStrategy); }, /** * */ oAuthUnbind : function(ownerid, next) { this.removeCredential( ownerid, this.getName(), next ); }, oAuthBinder: function(req, accessToken, refreshToken, params, profile, done) { var self = this, modelName = 'account_auth', accountInfo = req.remoteUser, accountId = accountInfo.getId(); if(!profile) profile = {} // upsert oAuth document var filter = { owner_id : accountId, type : 'oauth', oauth_provider : this.getName() }; var struct = { owner_id : accountId, password : accessToken, type : 'oauth', oauth_provider : this.getName(), oauth_refresh : refreshToken || '', oauth_profile : profile._json ? profile._json : profile }; if (params.expires_in) { struct.oauth_token_expire = params.expires_in; } // @todo upserts don't work with mongoose middleware // create a dao helper for filter -> model upsert. this._dao.find(modelName, filter, function(err, result) { var next = done; if (err) { done( err, req.remoteUser ); } else { if (result) { self._dao.updateProperties( modelName, result.id, struct, function(err) { next( err, accountInfo ); } ); } else { var model = self._dao.modelFactory(modelName, struct); self._dao.create(model, function(err, result) { next( err, accountInfo ); }); } self._dao.updateColumn('channel', { owner_id : accountId, action : { $regex : self.getName() + '\.*' } }, { _available : true }); } }); }, _oAuthRefresh : function(token, next) { this._logger.call(this, 'oAuth Token Expired But No Refresh Implementation', 'error'); next(); }, oAuthRefresh : function(authModel) { var refreshToken = authModel.getOAuthRefresh(), self = this; this._oAuthRefresh(refreshToken, function(err, refreshStruct) { if (!err && refreshStruct) { self._dao.updateProperties( 'account_auth', authModel.id, { password : refreshStruct.access_token, oauth_token_expire : refreshStruct.expires_in }, function(err) { if (!err) { self._logger.call(self, self.getName() + ':OAuthRefresh:' + authModel.owner_id); } else { self._logger.call(self, err, 'error'); } } ); } }); }, // -------------------------------------------------- STREAMING AND POD DATA _httpGet: function(url, next, headers, options) { var headerStruct = { 'User-Agent': 'request' }; var params = { url : url, method : 'GET' }; if (headers) { for (var k in headers) { if (headers.hasOwnProperty(k)) { headerStruct[k] = headers[k]; } } } params.headers = headerStruct; params.gzip = true; if (options) { for (var k in options) { if (options.hasOwnProperty(k)) { params[k] = options[k]; } } } if (next) { request(params, function(error, res, body) { if (res && res.headers && -1 !== res.headers['content-type'].indexOf('json') || -1 !== res.headers['content-type'].indexOf('javascript')) { try { body = JSON.parse(body); } catch (e) { error = e.message; } } if (404 === res.statusCode) { next('Not Found', body, res.headers, res.statusCode); } else { next(error, body, res ? res.headers : null, res ? res.statusCode : null); } } ); } else { return request(params); } }, _httpPost: function(url, postData, next, headers, options) { var headerStruct = { 'User-Agent': 'request' }; var params = { url : url, method : 'POST', json : postData } if (headers) { for (var k in headers) { if (headers.hasOwnProperty(k)) { headerStruct[k] = headers[k]; } } } params.headers = headerStruct; params.gzip = true; if (options) { for (var k in options) { if (options.hasOwnProperty(k)) { params[k] = options[k]; } } } if (next) { request(params, function(error, res, body) { next(error, body, res ? res.headers : null); }); } else { return request(params); } }, _httpPut: function(url, putData, next, headers, options) { var headerStruct = { 'User-Agent': 'request' }, params = { url : url, method : 'PUT' }; if (options) { for (var k in options) { if (options.hasOwnProperty(k)) { params[k] = options[k]; } } } if (headers) { for (var k in headers) { if (headers.hasOwnProperty(k)) { headerStruct[k] = headers[k]; } } } params.headers = headerStruct; params.gzip = true; if (putData) { params.json = putData; } if (next) { request(params, function(error, res, body) { next(error, body, res ? res.headers : null); }); } else { return request(params); } }, // -------------------------------------------------- CDN HELPERS _httpStreamToFile : function(url, outFile, cb, persist, headers) { this.file.save( outFile, request.get( { url : url, headers : headers } ), persist, cb ); }, _createChannelDir : function(prefix, channel, action, next) { var self = this, dDir = prefix + '/channels/'; if (undefined != channel.owner_id) { dDir += channel.owner_id + '/'; } dDir += this.getName() + '/' + action + '/' + channel.id + '/'; return dDir; }, // @deprecated _rmChannelDir : function(pfx, channel, action, next) { var self = this, files, file, dDir = pfx + '/channels/'; if (undefined != channel.owner_id) { dDir += channel.owner_id + '/'; } dDir += this.getName() + '/' + action + '/' + channel.id + '/'; app.helper.rmdir(dDir, function(err) { if (err) { self.log(err.message, channel, 'error'); } if (next) { next(err); } }); }, _expireChannelDir : function(pfx, channel, action, ageDays) { var self = this, dDir = pfx + '/channels/'; maxTime = (new Date()).getTime() - (ageDays * 24 * 60 * 60 * 1000); if (undefined != channel.owner_id) { dDir += channel.owner_id + '/'; } dDir += this.getName() + '/' + action + '/' + channel.id + '/'; fs.readdir(dDir, function(err, files) { if (err) { self.log(err, channel, 'error'); } else { for (var f = 0; f < files.length; f++) { (function(fileName) { fs.stat(fileName, function(err, stat) { if (err) { self.log(err, channel, 'error'); } else { if (stat.mtime.getTime() < maxTime) { fs.unlink(fileName, function(err) { if (err) { self.log(err, channel, 'error'); } }); } } }); })(dDir + files[f]); } } }); }, // -------- Data Directory interfaces // returns the file based data dir for this pod getDataDir: function(channel, action, next) { return this._createChannelDir('', channel, action, next); }, // remove datadir and all of its contents rmDataDir : function(channel, action, next) { return this._rmChannelDir('', channel, action, next); }, // -------- CDN Directory interfaces // gets public cdn getCDNDir : function(channel, action, suffix) { var prefix = this.options.cdnBasePath + (suffix ? ('/' + suffix) : ''); return this._createChannelDir(prefix, channel, action); }, getCDNBaseDir : function(suffix) { return this.options.cdnBasePath + (suffix ? ('/' + suffix) : ''); }, // removes cdn dir and all of its contents rmCDNDir : function(channel, action, next) { return this._rmChannelDir(this.options.cdnBasePath, channel, action, next); }, // removes cdn data by age expireCDNDir : function(channel, action, ageDays) { return this._expireChannelDir(this.options.cdnBasePath, channel, action, ageDays); }, getCDNURL : function() { return this.options.cdnPublicBaseURL; }, // ------------------------------------------------------------------------- // ----------------------------------------------- CHANNEL BRIDGE INTERFACE /** * Adds an Action to this Pod, attaches $resource to the action and * unpacks metadata for capabilities * * @param ActionProto {Object} Action Object */ /* add : function(ActionProto) { this._actionProtos.push(ActionProto); }, */ // DUMMY - DEPRECATED add : function() { }, /* * Runs the setup function for the pod action, if one exists * * @todo separate setup scope for pod and channel action. A pod setup for example * might install some default channels into the account for immediate use (such as * autoInstall does) * * @param action {String} Configured pod Action * @param channel {Channel} initialized channel * @param accountInfo {Object} AccountInfo Structure for Authenticated Account * @paran next {Function} callback */ setup : function(action, channel, accountInfo, auth, next) { var self = this, config = this.getConfig(); if (!next && 'function' === typeof auth) { next = auth; } else { if (self.isOAuth()) { if (!auth.oauth) { auth.oauth = {}; } _.each(config.oauth, function(value, key) { auth.oauth[key] = value; }); } accountInfo._setupAuth = auth; } if (this.actions[action] && this.actions[action].setup) { this.actions[action].setup(channel, accountInfo, function(err) { if (err) { self.log(err, channel, 'error'); } next(err); }); } else { next(false); } }, /* * * * */ update : function(action, channel, accountInfo, auth, next) { var self = this, config = this.getConfig(); if (!next && 'function' === typeof auth) { next = auth; } else { if (self.isOAuth()) { if (!auth.oauth) { auth.oauth = {}; } _.each(config.oauth, function(value, key) { auth.oauth[key] = value; }); } accountInfo._setupAuth = auth; } if (this.actions[action] && this.actions[action].update) { this.actions[action].update(channel, accountInfo, function(err) { if (err) { self.log(err, channel, 'error'); } next(err); }); } else { next(false); } }, // tries to drop any duplicates from db _dupTeardown : function(channel, next) { var filter = { channel_id : channel.id }, modelName = this.getDataSourceName('dup'); this._dao.removeFilter(modelName, filter, next); }, // expires duplicates _expireDups : function(channel, numDays, next) { var filter, modelName = this.getDataSourceName('dup'), maxTime = (new Date()).getTime() - (numDays * 24 * 60 * 60 * 1000); if (channel) { filter = { channel_id : channel.id } } this._dao.expire(modelName, filter, maxTime, next); }, /** * Runs the teardown for a pod action if one exists */ teardown : function(action, channel, accountInfo, auth, next) { var self = this, config = this.getConfig(); if (!next && 'function' === typeof auth) { next = auth; } else { if (self.isOAuth()) { if (!auth.oauth) { auth.oauth = {}; } _.each(config.oauth, function(value, key) { auth.oauth[key] = value; }); } accountInfo._setupAuth = auth; } if (this.actions[action] && this.actions[action].teardown) { if (this.getTrackDuplicates()) { // confirm teardown and drop any dup tracking from database this.actions[action].teardown(channel, accountInfo, function(err) { if (err) { self.log(err, channel, 'error'); } next(err); self._dupTeardown(channel); }); } else { this.actions[action].teardown(channel, accountInfo, function(err) { if (err) { self.log(err, channel, 'error'); } next(err); }); } } else { if (this.getTrackDuplicates()) { self._dupTeardown(channel); } next(false); } }, /** * * * */ bindUserAuth : function(sysImports, ownerId, next) { var self = this, config = this.getConfig(), cfgClone; if (!sysImports.auth) { sysImports.auth = {}; } if ( (self.isOAuth() && !sysImports.auth.oauth) || (self.isIssuerAuth() && !sysImports.auth.issuer_token) ) { self._dao.getPodAuthTokens(ownerId, this, function(err, tokenStruct) { if (err) { next(err); } else { sysImports.auth = {}; if (self.isOAuth()) { // apply token struct into config (which becomes derived sysImports.auth.oauth) cfgClone = JSON.parse(JSON.stringify(config.oauth)); _.each(tokenStruct, function(value, key) { cfgClone[key] = value; }); sysImports.auth.oauth = cfgClone; } else if (self.isIssuerAuth()) { sysImports.auth.issuer_token = tokenStruct; } next(false, sysImports); } }); } else { next(false, sysImports); } }, /** * Invokes the action * * @param action {String} action name * @param channel {Object} Channel model * @param imports {Object} Imports Map * @param sysImports {Object} System Imports and Account Info * @param contentParts * @paran next {Function} callback */ invoke: function(action, channel, imports, sysImports, contentParts, next, invokeOverride) { var self = this, errStr, parsingError = false, schemePtr, mixedTypes = [ 'mixed', 'array', 'object' ] if (this.actions[action].invoke) { if (!channel.config) { channel.config = {}; } if (!contentParts) { contentParts = { _files : [] } } try { // apply channel config defaults into imports, if required // fields don't already exist var actionSchema = this.getAction(action), missingFields = (actionSchema.imports && actionSchema.imports.required && actionSchema.imports.required.length) ? actionSchema.imports.required : []; // apply config defaults var configDefaults = this.getActionConfigDefaults(action), configSchema = this.getActionConfig(action).properties, importSchema = this.getActionImports(action).properties; _.each(configDefaults, function(value, key) { if (!channel.config[key]) { channel.config[key] = value; } }); // transpose from config to imports (no need to reference channel.config in pods) _.each(channel.config, function(value, key) { // convert to boolean if not already if (configSchema[key] && 'boolean' === configSchema[key].type) { channel.config[key] = helper.isTruthy(value) ? true : false; } if (!imports[key]) { imports[key] = channel.config[key]; } }); // derive import defaults var importDefaults = this.getActionImportDefaults(action); _.each(importDefaults, function(value, key) { if (!imports[key]) { imports[key] = value; } }); for (var k in imports) { // trim empty import if (imports.hasOwnProperty(k) && '' === imports[k]) { delete imports[k]; continue; } if (missingFields.length && missingFields.indexOf(k) > -1 ){ missingFields.splice(missingFields.indexOf(k) ,1); } var mixed; if( importSchema[k] && importSchema[k].type || configSchema[k] && configSchema[k].type ) { p_errStr = null; value = imports[k]; if (importSchema[k]) { schemePtr = importSchema[k]; } else if (configSchema[k]) { schemePtr = configSchema[k]; } type = schemePtr.type.toLowerCase(); // if ('number' === type) { pValue = helper.stringToFloat(value); if (0 !== pValue && !pValue) { p_errStr = k + ': String cannot be converted to Number'; } else { imports[k] = pValue; } } else if ('integer' === type) { pValue = helper.stringToInt(value); if (0 !== pValue && !pValue) { p_errStr = k + ': String cannot be converted to Integer'; } else { imports[k] = pValue; } } else if ('boolean' === type) { var pValue = helper.stringToBoolean(value); if (pValue == null) { p_errStr = k + ': String cannot be converted to Boolean'; } else { imports[k] = pValue; } } else if (-1 !== mixedTypes.indexOf(type) ) { if ('object' === type && helper.isObject(value)) { imports[k] = value; } else if ('array' === type && helper.isArray(value)) { imports[k] = value; } else { pValue = helper.stringToJson(value); if (pValue) { imports[k] = pValue; } else if(!pValue){ i