bip-pod
Version:
Bipio's Pod bridge
1,956 lines (1,619 loc) • 65.8 kB
JavaScript
/**
*
* 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