diff --git a/lib/apis.js b/lib/apis.js
index 4068e0b..f576953 100644
--- a/lib/apis.js
+++ b/lib/apis.js
@@ -8,7 +8,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
var express = require('express-lazy');
var fs = PromiseA.promisifyAll(require('fs'));
var path = require('path');
- var localCache = { rests: {}, pkgs: {} };
+ var localCache = { rests: {}, pkgs: {}, assets: {} };
var promisableRequest = require('./common').promisableRequest;
var rejectableRequest = require('./common').rejectableRequest;
var crypto = require('crypto');
@@ -32,7 +32,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
}
*/
- function isThisClientAllowedToUseThisPkg(myConf, clientUrih, pkgId) {
+ function isThisClientAllowedToUseThisPkg(req, myConf, clientUrih, pkgId) {
var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);
return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
@@ -51,12 +51,23 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
return true;
}
- if (clientUrih === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === pkgId) {
+ console.log('#################################################');
+ console.log('assets.' + xconfx.setupDomain);
+ console.log('assets.' + clientUrih);
+ console.log(req.clientAssetsUri);
+ console.log(pkgId);
+
+ if (req.clientAssetsUri === ('assets.' + clientUrih) && -1 !== [ 'session', 'session@oauth3.org', 'azp@oauth3.org', 'issuer@oauth3.org' ].indexOf(pkgId)) {
// fallthrough
return true;
- } else {
- return null;
}
+
+ if (clientUrih === ('api.' + xconfx.setupDomain) && -1 !== ['org.oauth3.consumer', 'azp@oauth3.org', 'oauth3.org'].indexOf(pkgId)) {
+ // fallthrough
+ return true;
+ }
+
+ return null;
});
}
@@ -211,10 +222,698 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
rejectableRequest(req, res, promise, "[walnut@daplie.com] required account (not /public)");
}
- function loadRestHelper(myConf, clientUrih, pkgId) {
- var pkgPath = path.join(myConf.restPath, pkgId);
+ function loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath) {
var pkgLinks = [];
pkgLinks.push(pkgId);
+ var pkgRestApi;
+ var pkgDeps = {};
+ var myApp;
+ var pkgPathApi;
+
+ pkgPathApi = pkgPath;
+ if (pkg.walnut) {
+ pkgPathApi = path.join(pkgPath, pkg.walnut);
+ }
+ pkgRestApi = require(pkgPathApi);
+
+ 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);
+
+ 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;
+ }
+ var scope = req.oauth3.token.scope || req.oauth3.token.scp || req.oauth3.token.grants;
+ if ('string' !== typeof scope) {
+ res.send({ error: { message: "Token must contain a grants string in 'scope'", code: "E_NO_GRANTS" } });
+ return;
+ }
+
+ tokenScopes = scope.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;
+ var _get_response;
+ myApp.use('/', function preHandler(req, res, next) {
+ //if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
+ return getSiteConfig(clientUrih).then(function (siteConfig) {
+ //if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
+ 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;
+ }
+ });
+
+ Object.defineProperty(req, 'GetResponse', {
+ enumerable: true
+ , configurable: false
+ , get: function () {
+ if (_get_response) {
+ return _get_response;
+ }
+ _get_response = {
+ saveSubscriber: function (email, opts) {
+ var config = siteConfig['getresponse@daplie.com'];
+ var customFields = [];
+ Object.keys(config.customFields).forEach(function (name) {
+ if (typeof opts[name] !== 'undefined') {
+ customFields.push({
+ customFieldId: config.customFields[name]
+ , value: [ String(opts[name]) ]
+ });
+ }
+ });
+
+ return request({
+ method: 'POST'
+ , url: 'https://api.getresponse.com/v3/contacts'
+ , headers: { 'X-Auth-Token': 'api-key ' + config.apiKey }
+ , json: true
+ , body: {
+ name: opts.name
+ , email: email
+ , ipAddress: opts.ipAddress
+ , campaign: { campaignId: config.campaignId }
+ , customFieldValues: customFields
+ }
+ }).then(function (resp) {
+ if (resp.statusCode === 202) {
+ return;
+ }
+ return PromiseA.reject(resp.body.message);
+ });
+ }
+ };
+
+ return _get_response;
+ }
+ });
+
+ 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')
+ , 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());
+ }
+ function getResponseList() {
+ return apiDeps.Promise.resolve(req.GetResponse);
+ }
+
+ // 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
+
+ , 'getresponse@daplie.com': getResponseList
+ //
+ // 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('Invalid signature. Are you even Twilio?');
+ 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);
+ }
+ if (siteConfig[capname]) {
+ var service = siteConfig[capname].service || siteConfig[capname];
+ if (caps[service]) {
+ return caps[service](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(pkgRestApi.create({
+ etcpath: xconfx.etcpath
+ }/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
+
+ //if (xconfx.debug) { console.log('[api.js] got 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];
+ });
+ }
+ function loadRestHelperAssets(myConf, clientUrih, pkg, pkgId, pkgPath) {
+ var myApp;
+ var pkgDeps = {};
+ var pkgRestAssets = require(path.join(pkgPath, 'assets.js'));
+
+ 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);
+
+ 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;
+ }
+ var scope = req.oauth3.token.scope || req.oauth3.token.scp || req.oauth3.token.grants;
+ if ('string' !== typeof scope) {
+ res.send({ error: { message: "Token must contain a grants string in 'scope'", code: "E_NO_GRANTS" } });
+ return;
+ }
+
+ tokenScopes = scope.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').cookieOauth3);
+ myApp.use('/', function (req, res, next) {
+ console.log('########################################### session ###############################');
+ console.log('req.url', req.url);
+ console.log('req.oauth3', req.oauth3);
+ next();
+ });
+ myApp.post('/assets/issuer@oauth3.org/session', require('./oauth3').attachOauth3, function (req, res) {
+ console.log('get the session');
+ console.log(req.url);
+ console.log("req.cookies:");
+ console.log(req.cookies);
+ console.log("req.oauth3:");
+ console.log(req.oauth3);
+ res.cookie('jwt', req.oauth3.encodedToken, { domain: req.clientAssetsUri, path: '/assets', httpOnly: true });
+ //req.url;
+ res.send({ success: true });
+ });
+
+ // TODO delete these caches when config changes
+ myApp.use('/', function preHandler(req, res, next) {
+ //if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
+ return getSiteConfig(clientUrih).then(function (siteConfig) {
+ //if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
+
+ Object.defineProperty(req, 'getSiteConfig', {
+ enumerable: true
+ , configurable: false
+ , writable: false
+ , value: function getSiteConfigProp(section) {
+ 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);
+ }
+ });
+
+ req._walnutOriginalUrl = req.url;
+ // "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
+ req.url = req.url.replace(/\/(api|assets)\//, '').replace(/.*\/(api|assets)\//, '').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
+ //
+ function myAppWrapper(req, res, next) {
+ return myApp(req, res, next);
+ }
+ Object.keys(myApp).forEach(function (key) {
+ myAppWrapper[key] = myApp[key];
+ });
+ myAppWrapper.use = function () { myApp.use.apply(myApp, arguments); };
+ myAppWrapper.get = function () { myApp.get.apply(myApp, arguments); };
+ myAppWrapper.post = function () { myApp.use(function (req, res, next) { next(); }); /*throw new Error("assets may not handle POST");*/ };
+ myAppWrapper.put = function () { throw new Error("assets may not handle PUT"); };
+ myAppWrapper.del = function () { throw new Error("assets may not handle DELETE"); };
+ myAppWrapper.delete = function () { throw new Error("assets may not handle DELETE"); };
+ return PromiseA.resolve(pkgRestAssets.create({
+ etcpath: xconfx.etcpath
+ }/*pkgConf*/, pkgDeps/*pkgDeps*/, myAppWrapper)).then(function (assetsHandler) {
+
+ //if (xconfx.debug) { console.log('[api.js] got handler'); }
+ myApp.use('/', function postHandler(req, res, next) {
+ req.url = req._walnutOriginalUrl;
+ next();
+ });
+
+ return assetsHandler || myApp;
+ });
+ }
+ function loadRestHelper(myConf, clientUrih, pkgId) {
+ var pkgPath = path.join(myConf.restPath, pkgId);
// TODO allow recursion, but catch cycles
return fs.lstatAsync(pkgPath).then(function (stat) {
@@ -233,510 +932,13 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
// 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);
-
- 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;
- }
- var scope = req.oauth3.token.scope || req.oauth3.token.scp || req.oauth3.token.grants;
- if ('string' !== typeof scope) {
- res.send({ error: { message: "Token must contain a grants string in 'scope'", code: "E_NO_GRANTS" } });
- return;
- }
-
- tokenScopes = scope.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;
- var _get_response;
- myApp.use('/', function preHandler(req, res, next) {
- //if (xconfx.debug) { console.log('[api.js] loading handler prereqs'); }
- return getSiteConfig(clientUrih).then(function (siteConfig) {
- //if (xconfx.debug) { console.log('[api.js] loaded handler site config'); }
- 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;
- }
- });
-
- Object.defineProperty(req, 'GetResponse', {
- enumerable: true
- , configurable: false
- , get: function () {
- if (_get_response) {
- return _get_response;
- }
- _get_response = {
- saveSubscriber: function (email, opts) {
- var config = siteConfig['getresponse@daplie.com'];
- var customFields = [];
- Object.keys(config.customFields).forEach(function (name) {
- if (typeof opts[name] !== 'undefined') {
- customFields.push({
- customFieldId: config.customFields[name]
- , value: [ String(opts[name]) ]
- });
- }
- });
-
- return request({
- method: 'POST'
- , url: 'https://api.getresponse.com/v3/contacts'
- , headers: { 'X-Auth-Token': 'api-key ' + config.apiKey }
- , json: true
- , body: {
- name: opts.name
- , email: email
- , ipAddress: opts.ipAddress
- , campaign: { campaignId: config.campaignId }
- , customFieldValues: customFields
- }
- }).then(function (resp) {
- if (resp.statusCode === 202) {
- return;
- }
- return PromiseA.reject(resp.body.message);
- });
- }
- };
-
- return _get_response;
- }
- });
-
- 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());
- }
- function getResponseList() {
- return apiDeps.Promise.resolve(req.GetResponse);
- }
-
- // 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
-
- , 'getresponse@daplie.com': getResponseList
- //
- // 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('Invalid signature. Are you even Twilio?');
- 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);
- }
- if (siteConfig[capname]) {
- var service = siteConfig[capname].service || siteConfig[capname];
- if (caps[service]) {
- return caps[service](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();
+ return loadRestHelperApi(myConf, clientUrih, pkg, pkgId, pkgPath).then(function (stuff) {
+ return loadRestHelperAssets(myConf, clientUrih, pkg, pkgId, pkgPath).then(function (assetsHandler) {
+ stuff.assetsHandler = assetsHandler;
+ return stuff;
});
});
- 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) {
-
- //if (xconfx.debug) { console.log('[api.js] got 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];
- });
});
});
}
@@ -784,13 +986,14 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
// 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(/\/$/, '');
+ var clientUrih = req.hostname.replace(/^(api|assets)\./, '') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
+ var clientApiUri = req.hostname.replace(/^(api|assets)\./, 'api.') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, '');
+ var clientAssetsUri = req.hostname.replace(/^(api|assets)\./, 'assets.') + req.url.replace(/\/(api|assets)\/.*/, '/').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 pkgId = req.url.replace(/.*\/(api|assets)\//, '').replace(/^\//, '').replace(/\/.*/, '');
var now = Date.now();
var hasBeenHandled = false;
@@ -801,6 +1004,12 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
, writable: false
, value: 'https://' + clientApiUri + '/api/' + pkgId
});
+ Object.defineProperty(req, 'assetsUrlPrefix', {
+ enumerable: true
+ , configurable: false
+ , writable: false
+ , value: 'https://' + clientAssetsUri + '/assets/' + pkgId
+ });
Object.defineProperty(req, 'experienceId', {
enumerable: true
, configurable: false
@@ -813,6 +1022,12 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
, writable: false
, value: clientApiUri
});
+ Object.defineProperty(req, 'clientAssetsUri', {
+ enumerable: true
+ , configurable: false
+ , writable: false
+ , value: clientAssetsUri
+ });
Object.defineProperty(req, 'apiId', {
enumerable: true
, configurable: false
@@ -838,19 +1053,36 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
});
// TODO cache permission (although the FS is already cached, NBD)
- var promise = isThisClientAllowedToUseThisPkg(xconfx, clientUrih, pkgId).then(function (yes) {
+ var promise = isThisClientAllowedToUseThisPkg(req, xconfx, clientUrih, pkgId).then(function (yes) {
//if (xconfx.debug) { console.log('[api.js] azp is allowed?', yes); }
if (!yes) {
notConfigured(req, res);
return null;
}
+ function handleWithHandler() {
+ if (/\/assets\//.test(req.url) || /(^|\.)assets\./.test(req.hostname)) {
+ if (localCache.assets[pkgId]) {
+ if ('function' !== typeof localCache.assets[pkgId].handler) { console.log('localCache.assets[pkgId]'); console.log(localCache.assets[pkgId]); }
+ localCache.assets[pkgId].handler(req, res, next);
+ } else {
+ next();
+ return true;
+ }
+ } else {
+ localCache.rests[pkgId].handler(req, res, next);
+ }
+ }
+
if (localCache.rests[pkgId]) {
- localCache.rests[pkgId].handler(req, res, next);
+ if (handleWithHandler()) {
+ return;
+ }
hasBeenHandled = true;
if (now - localCache.rests[pkgId].createdAt > staleAfter) {
localCache.rests[pkgId] = null;
+ localCache.assets[pkgId] = null;
}
}
@@ -866,13 +1098,16 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
}
localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
+ localCache.assets[pkgId] = { handler: myHandler.assetsHandler, createdAt: now };
if (!hasBeenHandled) {
- //if (xconfx.debug) { console.log('[api.js] not configured'); }
- myHandler.handler(req, res, next);
+ if (handleWithHandler()) {
+ return;
+ }
}
});
}
});
+
rejectableRequest(req, res, promise, "[walnut@daplie.com] load api package");
});
};
diff --git a/lib/main.js b/lib/main.js
index c3d3515..8fd8eff 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,6 +1,6 @@
'use strict';
-module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi) {
+module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets) {
var PromiseA = require('bluebird');
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
@@ -293,10 +293,27 @@ module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi
// TODO handle assets.example.com/sub/assets/com.example.xyz/
app.use('/api', require('connect-send-error').error());
+ app.use('/assets', require('connect-send-error').error());
app.use('/', function (req, res, next) {
- // If this doesn't look like an API we can move along
- if (!/\/api(\/|$)/.test(req.url)) {
- // /^api\./.test(req.hostname) &&
+ // If this doesn't look like an API or assets we can move along
+
+ /*
+ console.log('.');
+ console.log('[main.js] req.url, req.hostname');
+ console.log(req.url);
+ console.log(req.hostname);
+ console.log('.');
+ */
+
+ if (!/\/(api|assets)(\/|$)/.test(req.url)) {
+ //console.log('[main.js] api|assets');
+ next();
+ return;
+ }
+
+ // keep https://assets.example.com/assets but skip https://example.com/assets
+ if (/\/assets(\/|$)/.test(req.url) && !/(^|\.)(api|assets)(\.)/.test(req.hostname) && !/^[0-9\.]+$/.test(req.hostname)) {
+ //console.log('[main.js] skip');
next();
return;
}
@@ -325,6 +342,7 @@ module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi
return;
});
app.use('/', errorIfApi);
+ app.use('/', errorIfAssets);
app.use('/', serveStatic);
app.use('/', serveApps);
diff --git a/lib/oauth3.js b/lib/oauth3.js
index fc8ea59..c89df8d 100644
--- a/lib/oauth3.js
+++ b/lib/oauth3.js
@@ -181,6 +181,50 @@ function deepFreeze(obj) {
Object.freeze(obj);
}
+function cookieOauth3(req, res, next) {
+ req.oauth3 = {};
+
+ var token = req.cookies.jwt;
+
+ req.oauth3.encodedToken = token;
+ req.oauth3.verifyAsync = function (jwt) {
+ return verifyToken(jwt || token);
+ };
+
+ return verifyToken(token).then(function (decoded) {
+ req.oauth3.token = decoded;
+ if (!decoded) {
+ return null;
+ }
+
+ var ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
+ req.oauth3.ppid = ppid;
+ req.oauth3.accountIdx = ppid+'@'+decoded.iss;
+
+ var hash = require('crypto').createHash('sha256').update(req.oauth3.accountIdx).digest('base64');
+ hash = hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+/g, '');
+ req.oauth3.accountHash = hash;
+
+ req.oauth3.rescope = function (sub) {
+ // TODO: this function is supposed to convert PPIDs of different parties to some account
+ // ID that allows application to keep track of permisions and what-not.
+ return PromiseA.resolve(sub || hash);
+ };
+ }).then(function () {
+ deepFreeze(req.oauth3);
+ //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
+ next();
+ }, function (err) {
+ if ('E_NO_TOKEN' === err.code) {
+ next();
+ return;
+ }
+ console.error('[walnut] cookie lib/oauth3 error:');
+ console.error(err);
+ res.send(err);
+ });
+}
+
function attachOauth3(req, res, next) {
req.oauth3 = {};
@@ -215,14 +259,15 @@ function attachOauth3(req, res, next) {
};
}).then(function () {
deepFreeze(req.oauth3);
- Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
+ //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
next();
}, function (err) {
- console.error('[walnut] lib/oauth3 error:');
+ console.error('[walnut] JWT lib/oauth3 error:');
console.error(err);
res.send(err);
});
}
module.exports.attachOauth3 = attachOauth3;
+module.exports.cookieOauth3 = cookieOauth3;
module.exports.verifyToken = verifyToken;
diff --git a/lib/worker.js b/lib/worker.js
index ce7d795..313c6d0 100644
--- a/lib/worker.js
+++ b/lib/worker.js
@@ -150,6 +150,10 @@ module.exports.create = function (webserver, xconfx, state) {
models: models
// TODO don't let packages use this directly
, Promise: PromiseA
+ , dns: PromiseA.promisifyAll(require('dns'))
+ , crypto: PromiseA.promisifyAll(require('crypto'))
+ , fs: PromiseA.promisifyAll(require('fs'))
+ , path: require('path')
};
var apiFactories = {
memstoreFactory: { create: scopeMemstore }
@@ -180,7 +184,7 @@ module.exports.create = function (webserver, xconfx, state) {
function setupMain() {
if (xconfx.debug) { console.log('[main] setup'); }
mainApp = express();
- require('./main').create(mainApp, xconfx, apiFactories, apiDeps, errorIfApi).then(function () {
+ require('./main').create(mainApp, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets).then(function () {
if (xconfx.debug) { console.log('[main] ready'); }
// TODO process.send({});
});
@@ -225,6 +229,24 @@ module.exports.create = function (webserver, xconfx, state) {
next();
}
+ function errorIfNotAssets(req, res, next) {
+ var hostname = req.hostname || req.headers.host;
+
+ if (!/^assets\.[a-z0-9\-]+/.test(hostname)) {
+ res.send({ error:
+ { message: "['" + hostname + req.url + "'] protected asset access is restricted to proper 'asset'-prefixed lowercase subdomains."
+ + " The HTTP 'Host' header must exist and must begin with 'assets.' as in 'assets.example.com'."
+ + " For development you may test with assets.localhost.daplie.me (or any domain by modifying your /etc/hosts)"
+ , code: 'E_NOT_API'
+ , _hostname: hostname
+ }
+ });
+ return;
+ }
+
+ next();
+ }
+
function errorIfApi(req, res, next) {
if (!/^api\./.test(req.headers.host)) {
next();
@@ -240,7 +262,25 @@ module.exports.create = function (webserver, xconfx, state) {
return;
}
- res.send({ error: { code: 'E_NO_IMPL', message: "not implemented" } });
+ res.send({ error: { code: 'E_NO_IMPL', message: "API not implemented" } });
+ }
+
+ function errorIfAssets(req, res, next) {
+ if (!/^assets\./.test(req.headers.host)) {
+ next();
+ return;
+ }
+
+ // has api. hostname prefix
+
+ // doesn't have /api url prefix
+ if (!/^\/assets\//.test(req.url)) {
+ console.log('[walnut/worker assets] req.url', req.url);
+ res.send({ error: { message: "missing /assets/ url prefix" } });
+ return;
+ }
+
+ res.send({ error: { code: 'E_NO_IMPL', message: "assets handler not implemented" } });
}
app.disable('x-powered-by');
@@ -258,8 +298,11 @@ module.exports.create = function (webserver, xconfx, state) {
}));
app.use('/api', recase);
+ var cookieParser = require('cookie-parser'); // signing is done in JWT
+
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.use('/api', errorIfNotApi);
+ app.use('/assets', /*errorIfNotAssets,*/ cookieParser()); // serializer { path: '/assets', httpOnly: true, sameSite: true/*, domain: assets.example.com*/ }
app.use('/', function (req, res) {
if (!(req.encrypted || req.secure)) {
// did not come from https