forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			819 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			819 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
 | |
|             if (!grants.every(function (grant) {
 | |
|               var scopes = grant.split(/\|/g);
 | |
|               return scopes.some(function (scp) {
 | |
|                 return tokenScopes.some(function (s) {
 | |
|                   return scp === s;
 | |
|                 });
 | |
|               });
 | |
|             })) {
 | |
|               res.send({ error: { message: "Token does not contain valid grants: '" + grants + "'", code: "E_NO_GRANTS" } });
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             next();
 | |
|           };
 | |
|         };
 | |
| 
 | |
|         var _getOauth3Controllers = pkgDeps.getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(
 | |
|           { sqlite3Sock: xconfx.sqlite3Sock, ipcKey: xconfx.ipcKey }
 | |
|         ).getControllers;
 | |
|         //require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
 | |
|         require('oauthcommon').inject(_getOauth3Controllers, myApp/*, pkgConf, pkgDeps*/);
 | |
| 
 | |
|         // 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/subpath/api should resolve to example.com#subapp
 | |
|       // sub.example.com/subpath/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");
 | |
|     });
 | |
|   };
 | |
| };
 |