814 lines
29 KiB
JavaScript
814 lines
29 KiB
JavaScript
'use strict';
|
|
|
|
module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
|
var PromiseA = apiDeps.Promise;
|
|
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
|
|
//var express = require('express');
|
|
var express = require('express-lazy');
|
|
var fs = PromiseA.promisifyAll(require('fs'));
|
|
var path = require('path');
|
|
var localCache = { rests: {}, pkgs: {} };
|
|
var promisableRequest = require('./common').promisableRequest;
|
|
var rejectableRequest = require('./common').rejectableRequest;
|
|
var crypto = require('crypto');
|
|
|
|
// TODO xconfx.apispath
|
|
xconfx.restPath = path.join(__dirname, '..', '..', 'packages', 'rest');
|
|
xconfx.apiPath = path.join(__dirname, '..', '..', 'packages', 'api');
|
|
xconfx.appApiGrantsPath = path.join(__dirname, '..', '..', 'packages', 'client-api-grants');
|
|
xconfx.appConfigPath = path.join(__dirname, '..', '..', 'var');
|
|
|
|
function notConfigured(req, res) {
|
|
var msg = "api package '" + req.pkgId + "' not configured for client uri '" + req.experienceId + "'"
|
|
+ ". To configure it place a new line '" + req.pkgId + "' in the file '/srv/walnut/packages/client-api-grants/" + req.experienceId + "'"
|
|
;
|
|
|
|
res.send({ error: { message: msg } });
|
|
}
|
|
|
|
/*
|
|
function isThisPkgInstalled(myConf, pkgId) {
|
|
}
|
|
*/
|
|
|
|
function isThisClientAllowedToUseThisPkg(myConf, clientUrih, pkgId) {
|
|
var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);
|
|
|
|
return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
|
|
return text.trim().split(/\n/);
|
|
}, function (err) {
|
|
if ('ENOENT' !== err.code) {
|
|
console.error(err);
|
|
}
|
|
return [];
|
|
}).then(function (apis) {
|
|
if (apis.some(function (api) {
|
|
if (api === pkgId) {
|
|
return true;
|
|
}
|
|
})) {
|
|
return true;
|
|
}
|
|
|
|
if (clientUrih === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === pkgId) {
|
|
// fallthrough
|
|
return true;
|
|
} else {
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
function getSitePackageConfig(clientUrih, pkgId) {
|
|
var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
|
|
return mkdirpAsync(siteConfigPath).then(function () {
|
|
return fs.readFileAsync(path.join(siteConfigPath, pkgId + '.json'), 'utf8').then(function (text) {
|
|
return JSON.parse(text);
|
|
}).then(function (data) { return data; }, function (/*err*/) { return {}; });
|
|
});
|
|
}
|
|
|
|
function getSiteConfig(clientUrih) {
|
|
// TODO test if the requesting package has permission to the root-level site config
|
|
var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
|
|
return mkdirpAsync(siteConfigPath).then(function () {
|
|
return fs.readFileAsync(path.join(siteConfigPath, 'config.json'), 'utf8').then(function (text) {
|
|
return JSON.parse(text);
|
|
}).then(function (data) { return data; }, function (/*err*/) { return {}; });
|
|
});
|
|
}
|
|
|
|
var modelsCache = {};
|
|
function getSiteStore(clientUrih, pkgId, dir) {
|
|
if (!modelsCache[clientUrih]) {
|
|
modelsCache[clientUrih] = apiFactories.systemSqlFactory.create({
|
|
init: true
|
|
, dbname: clientUrih // '#' is a valid file name character
|
|
});
|
|
}
|
|
|
|
// DB scopes:
|
|
// system (global)
|
|
// experience (per domain)
|
|
// api (per api)
|
|
// account (per user account)
|
|
// client (per 3rd party client)
|
|
|
|
// scope Experience to db
|
|
// scope Api by table
|
|
// scope Account and Client by column
|
|
return modelsCache[clientUrih].then(function (db) {
|
|
var wrap = require('masterquest-sqlite3');
|
|
|
|
return wrap.wrap(db, dir).then(function (models) {
|
|
//modelsCache[clientUrih] = PromiseA.resolve(models);
|
|
return models;
|
|
});
|
|
});
|
|
}
|
|
|
|
function accountRequiredById(req, res, next) {
|
|
var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
|
|
var tok = req.oauth3.token;
|
|
var accountId = req.params.accountId || '__NO_ID_GIVEN__';
|
|
var ppid;
|
|
|
|
if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
|
|
return ppid === accountId;
|
|
}).length) {
|
|
ppid = accountId;
|
|
}
|
|
|
|
if (tok.axs && tok.axs.filter(function (acc) {
|
|
return acc.id === accountId || acc.appScopedId === accountId;
|
|
}).length) {
|
|
ppid = accountId;
|
|
}
|
|
|
|
if (tok.acx && accountId === (tok.acx.appScopedId || tok.acx.id || tok.acx)) {
|
|
ppid = accountId;
|
|
}
|
|
|
|
if (!ppid) {
|
|
return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token"));
|
|
}
|
|
|
|
return req.oauth3.rescope(ppid).then(function (accountIdx) {
|
|
req.oauth3.accountIdx = accountIdx;
|
|
req.oauth3.ppid = ppid;
|
|
req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
|
|
//console.log('[com.daplie.walnut] accountIdx:', accountIdx);
|
|
//console.log('[com.daplie.walnut] ppid:', ppid);
|
|
|
|
next();
|
|
});
|
|
});
|
|
|
|
rejectableRequest(req, res, promise, "[com.daplie.walnut] attach account by id");
|
|
}
|
|
|
|
function accountRequired(req, res, next) {
|
|
// if this already has auth, great
|
|
if (req.oauth3.ppid) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// being public does not disallow authentication
|
|
if (req.isPublic && !req.oauth3.encodedToken) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
if (!req.oauth3.encodedToken) {
|
|
rejectableRequest(
|
|
req
|
|
, res
|
|
, PromiseA.reject(new Error("this secure resource requires an access token"))
|
|
, "[com.daplie.walnut] required account (not /public)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// verify the auth if it's here
|
|
var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
|
|
var tok = req.oauth3.token;
|
|
var ppid;
|
|
var err;
|
|
|
|
if (tok.sub) {
|
|
if (tok.sub.split(/,/g).length > 1) {
|
|
err = new Error("more than one 'sub' specified in token");
|
|
return PromiseA.reject(err);
|
|
}
|
|
ppid = tok.sub;
|
|
}
|
|
else if (tok.axs && tok.axs.length) {
|
|
if (tok.axs.length > 1) {
|
|
err = new Error("more than one 'axs' specified in token (also, update to using 'sub' instead)");
|
|
return PromiseA.reject(err);
|
|
}
|
|
ppid = tok.axs[0].appScopedId || tok.axs[0].id;
|
|
}
|
|
else if (tok.acx) {
|
|
ppid = tok.acx.appScopedId || tok.acx.id || tok.acx;
|
|
}
|
|
|
|
if (!ppid) {
|
|
return PromiseA.reject(new Error("could not determine accountId from access token"));
|
|
}
|
|
|
|
return req.oauth3.rescope(ppid).then(function (accountIdx) {
|
|
req.oauth3.accountIdx = accountIdx;
|
|
req.oauth3.ppid = ppid;
|
|
req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
|
|
|
|
next();
|
|
});
|
|
});
|
|
|
|
rejectableRequest(req, res, promise, "[com.daplie.walnut] required account (not /public)");
|
|
}
|
|
|
|
function loadRestHelper(myConf, clientUrih, pkgId) {
|
|
var pkgPath = path.join(myConf.restPath, pkgId);
|
|
var pkgLinks = [];
|
|
pkgLinks.push(pkgId);
|
|
|
|
// TODO allow recursion, but catch cycles
|
|
return fs.lstatAsync(pkgPath).then(function (stat) {
|
|
if (!stat.isFile()) {
|
|
return;
|
|
}
|
|
|
|
return fs.readFileAsync(pkgPath, 'utf8').then(function (text) {
|
|
pkgId = text.trim();
|
|
pkgPath = path.join(myConf.restPath, pkgId);
|
|
});
|
|
}, function () {
|
|
// ignore error
|
|
return;
|
|
}).then(function () {
|
|
// TODO should not require package.json. Should work with files alone.
|
|
return fs.readFileAsync(path.join(pkgPath, 'package.json'), 'utf8').then(function (text) {
|
|
var pkg = JSON.parse(text);
|
|
var pkgDeps = {};
|
|
var myApp;
|
|
|
|
if (pkg.walnut) {
|
|
pkgPath = path.join(pkgPath, pkg.walnut);
|
|
}
|
|
|
|
Object.keys(apiDeps).forEach(function (key) {
|
|
pkgDeps[key] = apiDeps[key];
|
|
});
|
|
Object.keys(apiFactories).forEach(function (key) {
|
|
pkgDeps[key] = apiFactories[key];
|
|
});
|
|
|
|
// TODO pull db stuff from package.json somehow and pass allowed data models as deps
|
|
//
|
|
// how can we tell which of these would be correct?
|
|
// deps.memstore = apiFactories.memstoreFactory.create(pkgId);
|
|
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
|
|
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);
|
|
|
|
// let's go with this one for now and the api can choose to scope or not to scope
|
|
pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);
|
|
|
|
console.log('DEBUG pkgPath', pkgPath);
|
|
myApp = express();
|
|
myApp.handlePromise = promisableRequest;
|
|
myApp.handleRejection = rejectableRequest;
|
|
myApp.grantsRequired = function (grants) {
|
|
if (!Array.isArray(grants)) {
|
|
throw new Error("Usage: app.grantsRequired([ 'name|altname|altname2', 'othergrant' ])");
|
|
}
|
|
|
|
if (!grants.length) {
|
|
return function (req, res, next) {
|
|
next();
|
|
};
|
|
}
|
|
|
|
return function (req, res, next) {
|
|
var tokenScopes;
|
|
|
|
if (!(req.oauth3 || req.oauth3.token)) {
|
|
// TODO some error generator for standard messages
|
|
res.send({ error: { message: "You must be logged in", code: "E_NO_AUTHN" } });
|
|
return;
|
|
}
|
|
if ('string' !== typeof req.oauth3.token.scp) {
|
|
res.send({ error: { message: "Token must contain a grants string in 'scp'", code: "E_NO_GRANTS" } });
|
|
return;
|
|
}
|
|
|
|
tokenScopes = req.oauth3.token.scp.split(/[,\s]+/mg);
|
|
if (-1 !== tokenScopes.indexOf('*')) {
|
|
// has full account access
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// every grant in the array must be present, though some grants can be satisfied
|
|
// by multiple scopes.
|
|
var missing = grants.filter(function (grant) {
|
|
return !grant.split('|').some(function (scp) {
|
|
return tokenScopes.indexOf(scp) !== -1;
|
|
});
|
|
});
|
|
if (missing.length) {
|
|
res.send({ error: { message: "Token missing required grants: '" + missing.join(',') + "'", code: "E_NO_GRANTS" } });
|
|
return;
|
|
}
|
|
|
|
next();
|
|
};
|
|
};
|
|
|
|
myApp.use('/', require('./oauth3').attachOauth3);
|
|
|
|
// TODO delete these caches when config changes
|
|
var _stripe;
|
|
var _stripe_test;
|
|
var _mandrill;
|
|
var _mailchimp;
|
|
var _twilio;
|
|
myApp.use('/', function preHandler(req, res, next) {
|
|
return getSiteConfig(clientUrih).then(function (siteConfig) {
|
|
Object.defineProperty(req, 'getSiteMailer', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: function getSiteMailerProp() {
|
|
var nodemailer = require('nodemailer');
|
|
var transport = require('nodemailer-mailgun-transport');
|
|
//var mailconf = require('../../../com.daplie.mailer/config.mailgun');
|
|
var mailconf = siteConfig['mailgun.org'];
|
|
var mailer = PromiseA.promisifyAll(nodemailer.createTransport(transport(mailconf)));
|
|
|
|
return mailer;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(req, 'getSiteConfig', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: function getSiteConfigProp(section) {
|
|
// deprecated
|
|
if ('com.daplie.tel' === section) {
|
|
section = 'tel@daplie.com';
|
|
}
|
|
return PromiseA.resolve((siteConfig || {})[section]);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(req, 'getSitePackageConfig', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: function getSitePackageConfigProp() {
|
|
return getSitePackageConfig(clientUrih, pkgId);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(req, 'getSiteStore', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: function getSiteStoreProp() {
|
|
var restPath = path.join(myConf.restPath, pkgId);
|
|
var apiPath = path.join(myConf.apiPath, pkgId);
|
|
var dir;
|
|
|
|
// TODO usage package.json as a falback if the standard location is not used
|
|
try {
|
|
dir = require(path.join(apiPath, 'models.js'));
|
|
} catch(e) {
|
|
dir = require(path.join(restPath, 'models.js'));
|
|
}
|
|
|
|
return getSiteStore(clientUrih, pkgId, dir);
|
|
}
|
|
});
|
|
|
|
/*
|
|
Object.defineProperty(req, 'getSitePayments', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: function getSitePaymentsProp() {
|
|
}
|
|
});
|
|
*/
|
|
// TODO allow third-party clients stripe ids destination
|
|
// https://stripe.com/docs/connect/payments-fees
|
|
Object.defineProperty(req, 'Stripe', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, get: function () {
|
|
_stripe = _stripe || require('stripe')(siteConfig['stripe.com'].live.secret);
|
|
return _stripe;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(req, 'StripeTest', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, get: function () {
|
|
_stripe_test = _stripe_test || require('stripe')(siteConfig['stripe.com'].test.secret);
|
|
return _stripe_test;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(req, 'Mandrill', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, get: function () {
|
|
if (!_mandrill) {
|
|
var Mandrill = require('mandrill-api/mandrill');
|
|
_mandrill = new Mandrill.Mandrill(siteConfig['mandrill.com'].apiKey);
|
|
_mandrill.messages.sendTemplateAsync = function (opts) {
|
|
return new PromiseA(function (resolve, reject) {
|
|
_mandrill.messages.sendTemplate(opts, resolve, reject);
|
|
});
|
|
};
|
|
}
|
|
return _mandrill;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(req, 'Mailchimp', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, get: function () {
|
|
var Mailchimp = require('mailchimp-api-v3');
|
|
_mailchimp = _mailchimp || new Mailchimp(siteConfig['mailchimp.com'].apiKey);
|
|
return _mailchimp;
|
|
}
|
|
});
|
|
|
|
var Twilio = require('twilio');
|
|
function twilioTel(/*opts*/) {
|
|
if (_twilio) {
|
|
return apiDeps.Promise.resolve(_twilio);
|
|
}
|
|
|
|
_twilio = new Twilio.RestClient(
|
|
siteConfig['twilio.com'].live.id
|
|
, siteConfig['twilio.com'].live.auth
|
|
);
|
|
return apiDeps.Promise.resolve(_twilio);
|
|
}
|
|
|
|
// TODO shared memory db
|
|
var mailgunTokens = {};
|
|
function validateMailgun(apiKey, timestamp, token, signature) {
|
|
// https://gist.github.com/coolaj86/81a3b61353d2f0a2552c
|
|
// (realized later)
|
|
// HAHA HAHA HAHAHAHAHA this is my own gist... so much more polite attribution
|
|
var scmp = require('scmp')
|
|
, crypto = require('crypto')
|
|
, mailgunExpirey = 15 * 60 * 1000
|
|
, mailgunHashType = 'sha256'
|
|
, mailgunSignatureEncoding = 'hex'
|
|
;
|
|
var actual
|
|
, adjustedTimestamp = parseInt(timestamp, 10) * 1000
|
|
, fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey)
|
|
;
|
|
|
|
if (!fresh) {
|
|
console.error('[mailgun] Stale Timestamp: this may be an attack');
|
|
console.error('[mailgun] However, this is most likely your fault\n');
|
|
console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n');
|
|
console.error('[mailgun] System Time: ' + new Date().toString());
|
|
console.error('[mailgun] Mailgun Time: ' + new Date(adjustedTimestamp).toString(), timestamp);
|
|
console.error('[mailgun] Delta: ' + (Date.now() - adjustedTimestamp));
|
|
return false;
|
|
}
|
|
|
|
if (mailgunTokens[token]) {
|
|
console.error('[mailgun] Replay Attack');
|
|
return false;
|
|
}
|
|
|
|
mailgunTokens[token] = true;
|
|
|
|
setTimeout(function () {
|
|
delete mailgunTokens[token];
|
|
}, mailgunExpirey + (5 * 1000));
|
|
|
|
actual = crypto.createHmac(mailgunHashType, apiKey)
|
|
.update(new Buffer(timestamp + token, 'utf8'))
|
|
.digest(mailgunSignatureEncoding)
|
|
;
|
|
return scmp(signature, actual);
|
|
}
|
|
|
|
function mailgunMail(/*opts*/) {
|
|
return apiDeps.Promise.resolve(req.getSiteMailer());
|
|
}
|
|
|
|
// Twilio Parameters are often 26 long
|
|
var bodyParserTwilio = require('body-parser').urlencoded({ limit: '4kb', parameterLimit: 100, extended: false });
|
|
// Mailgun has something like 50 parameters
|
|
var bodyParserMailgun = require('body-parser').urlencoded({ limit: '1024kb', parameterLimit: 500, extended: false });
|
|
function bodyMultiParserMailgun (req, res, next) {
|
|
var multiparty = require('multiparty');
|
|
var form = new multiparty.Form();
|
|
|
|
form.parse(req, function (err, fields/*, files*/) {
|
|
if (err) {
|
|
console.error('Error');
|
|
console.error(err);
|
|
res.end("Couldn't parse form");
|
|
return;
|
|
}
|
|
|
|
var body;
|
|
req.body = req.body || {};
|
|
Object.keys(fields).forEach(function (key) {
|
|
// TODO what if there were two of something?
|
|
// (even though there won't be)
|
|
req.body[key] = fields[key][0];
|
|
});
|
|
body = req.body;
|
|
|
|
next();
|
|
});
|
|
}
|
|
|
|
function daplieTel() {
|
|
return twilioTel().then(function (twilio) {
|
|
function sms(opts) {
|
|
// opts = { to, from, body }
|
|
return new apiDeps.Promise(function (resolve, reject) {
|
|
twilio.sendSms(opts, function (err, resp) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve(resp);
|
|
});
|
|
});
|
|
}
|
|
|
|
return {
|
|
sms: sms
|
|
, mms: function () { throw new Error('MMS Not Implemented'); }
|
|
};
|
|
});
|
|
}
|
|
|
|
var caps = {
|
|
//
|
|
// Capabilities for APIs
|
|
//
|
|
'email@daplie.com': mailgunMail // whichever mailer
|
|
, 'mailer@daplie.com': mailgunMail // whichever mailer
|
|
, 'mailgun@daplie.com': mailgunMail // specifically mailgun
|
|
, 'tel@daplie.com': daplieTel // whichever telephony service
|
|
, 'twilio@daplie.com': twilioTel // specifically twilio
|
|
, 'com.daplie.tel.twilio': twilioTel // deprecated alias
|
|
|
|
//
|
|
// Webhook Parsers
|
|
//
|
|
//, 'mailgun.urlencoded@daplie.com': function (req, res, next) { ... }
|
|
, 'mailgun.parsers@daplie.com': function (req, res, next) {
|
|
var chunks = [];
|
|
|
|
req.on('data', function (chunk) {
|
|
chunks.push(chunk);
|
|
});
|
|
req.on('end', function () {
|
|
});
|
|
|
|
function verify() {
|
|
var body = req.body;
|
|
var mailconf = siteConfig['mailgun.org'];
|
|
|
|
if (!body.timestamp) {
|
|
console.log('mailgun parser req.headers');
|
|
console.log(req.headers);
|
|
chunks.forEach(function (datum) {
|
|
console.log('Length:', datum.length);
|
|
//console.log(datum.toString('utf8'));
|
|
});
|
|
console.log('weird body');
|
|
console.log(body);
|
|
}
|
|
|
|
if (!validateMailgun(mailconf.apiKey, body.timestamp, body.token, body.signature)) {
|
|
console.error('Request came, but not from Mailgun');
|
|
console.error(req.url);
|
|
console.error(req.headers);
|
|
res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } });
|
|
return;
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
if (/urlencoded/.test(req.headers['content-type'])) {
|
|
console.log('urlencoded');
|
|
bodyParserMailgun(req, res, verify);
|
|
}
|
|
else if (/multipart/.test(req.headers['content-type'])) {
|
|
console.log('multipart');
|
|
bodyMultiParserMailgun(req, res, verify);
|
|
}
|
|
else {
|
|
console.log('no parser');
|
|
next();
|
|
}
|
|
}
|
|
, 'twilio.urlencoded@daplie.com': function (req, res, next) {
|
|
// TODO null for res and Promise instead of next?
|
|
return bodyParserTwilio(req, res, function () {
|
|
var signature = req.headers['x-twilio-signature'];
|
|
var auth = siteConfig['twilio.com'].live.auth;
|
|
var fullUrl = 'https://' + req.headers.host + req._walnutOriginalUrl;
|
|
var validSig = Twilio.validateRequest(auth, signature, fullUrl, req.body);
|
|
/*
|
|
console.log('Twilio Signature Check');
|
|
console.log('auth', auth);
|
|
console.log('sig', signature);
|
|
console.log('fullUrl', fullUrl);
|
|
console.log(req.body);
|
|
console.log('valid', validSig);
|
|
*/
|
|
if (!validSig) {
|
|
res.statusCode = 401;
|
|
res.setHeader('Content-Type', 'text/xml');
|
|
res.end('<Error>Invalid signature. Are you even Twilio?</Error>');
|
|
return;
|
|
}
|
|
// TODO session via db req.body.CallId req.body.smsId
|
|
next();
|
|
});
|
|
}
|
|
};
|
|
req.getSiteCapability = function (capname, opts, b, c) {
|
|
if (caps[capname]) {
|
|
return caps[capname](opts, b, c);
|
|
}
|
|
return apiDeps.Promise.reject(
|
|
new Error("['" + req.clientApiUri + '/' + pkgId + "'] "
|
|
+ "capability '" + capname + "' not implemented")
|
|
);
|
|
};
|
|
|
|
req._walnutOriginalUrl = req.url;
|
|
// "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
|
|
req.url = req.url.replace(/\/api\//, '').replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/');
|
|
next();
|
|
});
|
|
});
|
|
myApp.use('/public', function preHandler(req, res, next) {
|
|
// TODO authenticate or use guest user
|
|
req.isPublic = true;
|
|
next();
|
|
});
|
|
myApp.use('/accounts/:accountId', accountRequiredById);
|
|
myApp.use('/acl', accountRequired);
|
|
|
|
//
|
|
// TODO handle /accounts/:accountId
|
|
//
|
|
return PromiseA.resolve(require(pkgPath).create({
|
|
etcpath: xconfx.etcpath
|
|
}/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
|
|
|
|
myApp.use('/', function postHandler(req, res, next) {
|
|
req.url = req._walnutOriginalUrl;
|
|
next();
|
|
});
|
|
|
|
localCache.pkgs[pkgId] = { pkgId: pkgId, pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
|
|
pkgLinks.forEach(function (pkgLink) {
|
|
localCache.pkgs[pkgLink] = localCache.pkgs[pkgId];
|
|
});
|
|
return localCache.pkgs[pkgId];
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Read packages/apis/sub.sld.tld (forward dns) to find list of apis as tld.sld.sub (reverse dns)
|
|
// TODO packages/allowed_apis/sub.sld.tld (?)
|
|
// TODO auto-register org.oauth3.consumer for primaryDomain (and all sites?)
|
|
function loadRestHandler(myConf, clientUrih, pkgId) {
|
|
return PromiseA.resolve().then(function () {
|
|
if (!localCache.pkgs[pkgId]) {
|
|
return loadRestHelper(myConf, clientUrih, pkgId);
|
|
}
|
|
|
|
return localCache.pkgs[pkgId];
|
|
// TODO expire require cache
|
|
/*
|
|
if (Date.now() - localCache.pkgs[pkgId].createdAt < (5 * 60 * 1000)) {
|
|
return;
|
|
}
|
|
*/
|
|
}, function (/*err*/) {
|
|
// TODO what kind of errors might we want to handle?
|
|
return null;
|
|
}).then(function (restPkg) {
|
|
return restPkg;
|
|
});
|
|
}
|
|
|
|
var CORS = require('connect-cors');
|
|
var cors = CORS({ credentials: true, headers: [
|
|
'X-Requested-With'
|
|
, 'X-HTTP-Method-Override'
|
|
, 'Content-Type'
|
|
, 'Accept'
|
|
, 'Authorization'
|
|
], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
|
|
var staleAfter = (5 * 60 * 1000);
|
|
|
|
return function (req, res, next) {
|
|
cors(req, res, function () {
|
|
if (xconfx.debug) { console.log('[api.js] post cors'); }
|
|
|
|
// Canonical client names
|
|
// example.com should use api.example.com/api for all requests
|
|
// sub.example.com/api should resolve to sub.example.com
|
|
// example.com/subapp/api should resolve to example.com#subapp
|
|
// sub.example.com/subapp/api should resolve to sub.example.com#subapp
|
|
var clientUrih = req.hostname.replace(/^api\./, '') + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
|
|
var clientApiUri = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/$/, '');
|
|
// Canonical package names
|
|
// '/api/com.daplie.hello/hello' should resolve to 'com.daplie.hello'
|
|
// '/subapp/api/com.daplie.hello/hello' should also 'com.daplie.hello'
|
|
// '/subapp/api/com.daplie.hello/' may exist... must be a small api
|
|
var pkgId = req.url.replace(/.*\/api\//, '').replace(/^\//, '').replace(/\/.*/, '');
|
|
var now = Date.now();
|
|
var hasBeenHandled = false;
|
|
|
|
// Existing (Deprecated)
|
|
Object.defineProperty(req, 'apiUrlPrefix', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: 'https://' + clientApiUri + '/api/' + pkgId
|
|
});
|
|
Object.defineProperty(req, 'experienceId', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: clientUrih
|
|
});
|
|
Object.defineProperty(req, 'clientApiUri', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: clientApiUri
|
|
});
|
|
Object.defineProperty(req, 'apiId', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: pkgId
|
|
});
|
|
|
|
// New
|
|
Object.defineProperty(req, 'clientUrih', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
|
|
// (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
|
|
// NOTE: probably best to alias the name logically
|
|
, value: clientUrih
|
|
});
|
|
Object.defineProperty(req, 'pkgId', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: pkgId
|
|
});
|
|
|
|
// TODO cache permission (although the FS is already cached, NBD)
|
|
var promise = isThisClientAllowedToUseThisPkg(xconfx, clientUrih, pkgId).then(function (yes) {
|
|
if (!yes) {
|
|
notConfigured(req, res);
|
|
return null;
|
|
}
|
|
|
|
if (localCache.rests[pkgId]) {
|
|
localCache.rests[pkgId].handler(req, res, next);
|
|
hasBeenHandled = true;
|
|
|
|
if (now - localCache.rests[pkgId].createdAt > staleAfter) {
|
|
localCache.rests[pkgId] = null;
|
|
}
|
|
}
|
|
|
|
if (!localCache.rests[pkgId]) {
|
|
//return doesThisPkgExist
|
|
|
|
return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) {
|
|
if (!myHandler) {
|
|
notConfigured(req, res);
|
|
return;
|
|
}
|
|
|
|
localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
|
|
if (!hasBeenHandled) {
|
|
myHandler.handler(req, res, next);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
rejectableRequest(req, res, promise, "[com.daplie.walnut] load api package");
|
|
});
|
|
};
|
|
};
|