forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			264 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| module.exports.create = function (xconfx, apiFactories, apiDeps) {
 | |
|   var PromiseA = apiDeps.Promise;
 | |
|   var express = require('express');
 | |
|   var fs = PromiseA.promisifyAll(require('fs'));
 | |
|   var path = require('path');
 | |
|   var localCache = { apis: {}, pkgs: {} };
 | |
| 
 | |
|   // TODO xconfx.apispath
 | |
|   xconfx.apispath = path.join(__dirname, '..', '..', 'packages', 'apis');
 | |
| 
 | |
|   function notConfigured(req, res) {
 | |
|     res.send({ error: { message: "api '" + req.apiId + "' not configured for domain '" + req.experienceId + "'" } });
 | |
|   }
 | |
| 
 | |
|   function loadApi(conf, pkgConf, pkgDeps, packagedApi) {
 | |
|     function handlePromise(p) {
 | |
|       return p.then(function (api) {
 | |
|         packagedApi._api = api;
 | |
|         return api;
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     if (!packagedApi._promise_api) {
 | |
|       packagedApi._promise_api = getApi(conf, pkgConf, pkgDeps, packagedApi);
 | |
|     }
 | |
| 
 | |
|     return handlePromise(packagedApi._promise_api);
 | |
|   }
 | |
| 
 | |
|   function getApi(conf, pkgConf, pkgDeps, packagedApi) {
 | |
|     var PromiseA = pkgDeps.Promise;
 | |
|     var path = require('path');
 | |
|     var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.api.version || '')*/);
 | |
| 
 | |
|     // TODO needs some version stuff (which would also allow hot-loading of updates)
 | |
|     // TODO version could be tied to sha256sum
 | |
| 
 | |
|     return new PromiseA(function (resolve, reject) {
 | |
|       var myApp;
 | |
|       var ursa;
 | |
|       var promise;
 | |
| 
 | |
|       // TODO dynamic requires are a no-no
 | |
|       // can we statically generate a require-er? on each install?
 | |
|       // module.exports = { {{pkgpath}}: function () { return require({{pkgpath}}) } }
 | |
|       // requirer[pkgpath]()
 | |
|       myApp = pkgDeps.express();
 | |
|       myApp.disable('x-powered-by');
 | |
|       if (pkgDeps.app.get('trust proxy')) {
 | |
|         myApp.set('trust proxy', pkgDeps.app.get('trust proxy'));
 | |
|       }
 | |
|       if (!pkgConf.pubkey) {
 | |
|         /*
 | |
|           return ursa.createPrivateKey(pem, password, encoding);
 | |
|           var pem = myKey.toPrivatePem();
 | |
|           return jwt.verifyAsync(token, myKey.toPublicPem(), { ignoreExpiration: false && true }).then(function (decoded) {
 | |
|           });
 | |
|         */
 | |
|         ursa = require('ursa');
 | |
|         pkgConf.keypair = ursa.createPrivateKey(pkgConf.privkey, 'ascii');
 | |
|         pkgConf.pubkey = ursa.createPublicKey(pkgConf.pubkey, 'ascii'); //conf.keypair.toPublicKey();
 | |
|       }
 | |
| 
 | |
|       try {
 | |
|         packagedApi._apipkg = require(path.join(pkgpath, 'package.json'));
 | |
|         packagedApi._apiname = packagedApi._apipkg.name;
 | |
|         if (packagedApi._apipkg.walnut) {
 | |
|           pkgpath += '/' + packagedApi._apipkg.walnut;
 | |
|         }
 | |
|         promise = PromiseA.resolve(require(pkgpath).create(pkgConf, pkgDeps, myApp));
 | |
|       } catch(e) {
 | |
|         reject(e);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       promise.then(function () {
 | |
|         // TODO give pub/priv pair for app and all public keys
 | |
|         // packagedApi._api = require(pkgpath).create(pkgConf, pkgDeps, myApp);
 | |
|         packagedApi._api = require('express-lazy')();
 | |
|         packagedApi._api_app = myApp;
 | |
| 
 | |
|         //require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps);
 | |
|         pkgDeps.getOauth3Controllers =
 | |
|         packagedApi._getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(conf).getControllers;
 | |
|         require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
 | |
| 
 | |
|         // DEBUG
 | |
|         //
 | |
|         /*
 | |
|         packagedApi._api.use('/', function (req, res, next) {
 | |
|           console.log('[DEBUG pkgApiApp]', req.method, req.hostname, req.url);
 | |
|           next();
 | |
|         });
 | |
|         //*/
 | |
| 
 | |
|         // TODO fix backwards compat
 | |
| 
 | |
|         // /api/com.example.foo (no change)
 | |
|         packagedApi._api.use('/', packagedApi._api_app);
 | |
| 
 | |
|         // /api/com.example.foo => /api
 | |
|         packagedApi._api.use('/', function (req, res, next) {
 | |
|           var priorUrl = req.url;
 | |
|           req.url = '/api' + req.url.slice(('/api/' + packagedApi.id).length);
 | |
|           // console.log('api mangle 3:', req.url);
 | |
|           packagedApi._api_app(req, res, function (err) {
 | |
|             req.url = priorUrl;
 | |
|             next(err);
 | |
|           });
 | |
|         });
 | |
| 
 | |
|         // /api/com.example.foo => /
 | |
|         packagedApi._api.use('/api/' + packagedApi.id, function (req, res, next) {
 | |
|           // console.log('api mangle 2:', '/api/' + packagedApi.id, req.url);
 | |
|           // console.log(packagedApi._api_app.toString());
 | |
|           packagedApi._api_app(req, res, next);
 | |
|         });
 | |
| 
 | |
|         resolve(packagedApi._api);
 | |
|       }, reject);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // 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 loadApiHandler() {
 | |
|     return function handler(req, res, next) {
 | |
|       var name = req.experienceId;
 | |
|       var apiId = req.apiId;
 | |
|       var packagepath = path.join(xconfx.apispath, name);
 | |
| 
 | |
|       return fs.readFileAsync(packagepath, 'utf8').then(function (text) {
 | |
|         return text.trim().split(/\n/);
 | |
|       }, function () {
 | |
|         return [];
 | |
|       }).then(function (apis) {
 | |
|         return function (req, res, next) {
 | |
|           var apipath;
 | |
| 
 | |
|           if (!apis.some(function (api) {
 | |
|             if (api === apiId) {
 | |
|               return true;
 | |
|             }
 | |
|           })) {
 | |
|             if (req.experienceId === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === apiId) {
 | |
|               // fallthrough
 | |
|             } else {
 | |
|               return null;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           apipath = path.join(xconfx.apispath, apiId);
 | |
| 
 | |
|           if (!localCache.pkgs[apiId]) {
 | |
|             return fs.readFileAsync(path.join(apipath, 'package.json'), 'utf8').then(function (text) {
 | |
|               var pkg = JSON.parse(text);
 | |
|               var deps = {};
 | |
|               var myApp;
 | |
| 
 | |
|               if (pkg.walnut) {
 | |
|                 apipath = path.join(apipath, pkg.walnut);
 | |
|               }
 | |
| 
 | |
|               Object.keys(apiDeps).forEach(function (key) {
 | |
|                 deps[key] = apiDeps[key];
 | |
|               });
 | |
|               Object.keys(apiFactories).forEach(function (key) {
 | |
|                 deps[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(apiId);
 | |
|               // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
 | |
|               // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + apiId);
 | |
| 
 | |
|               // let's go with this one for now and the api can choose to scope or not to scope
 | |
|               deps.memstore = apiFactories.memstoreFactory.create(apiId);
 | |
| 
 | |
|               console.log('DEBUG apipath', apipath);
 | |
|               myApp = express();
 | |
|               //
 | |
|               // TODO handle /accounts/:accountId
 | |
|               //
 | |
|               return PromiseA.resolve(require(apipath).create({
 | |
|                 etcpath: xconfx.etcpath
 | |
|               }/*pkgConf*/, deps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
 | |
|                 localCache.pkgs[apiId] = { pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
 | |
|                 localCache.pkgs[apiId].handler(req, res, next);
 | |
|               });
 | |
|             });
 | |
|           }
 | |
|           else {
 | |
|             localCache.pkgs[apiId].handler(req, res, next);
 | |
|             // TODO expire require cache
 | |
|             /*
 | |
|             if (Date.now() - localCache.pkgs[apiId].createdAt < (5 * 60 * 1000)) {
 | |
|               return;
 | |
|             }
 | |
|             */
 | |
|           }
 | |
|         };
 | |
|       }, function (/*err*/) {
 | |
|         return null;
 | |
|       }).then(function (handler) {
 | |
| 
 | |
|         // keep object reference intact
 | |
|         // DO NOT cache non-existant api
 | |
|         if (handler) {
 | |
|           localCache.apis[name].handler = handler;
 | |
|         } else {
 | |
|           handler = notConfigured;
 | |
|         }
 | |
|         handler(req, res, next);
 | |
|       });
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   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" ] });
 | |
| 
 | |
|   return function (req, res, next) {
 | |
|     cors(req, res, function () {
 | |
|       var experienceId = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
 | |
|       var apiId = req.url.replace(/.*\/api\//, '').replace(/\/.*/, '');
 | |
| 
 | |
|       Object.defineProperty(req, 'experienceId', {
 | |
|         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: experienceId
 | |
|       });
 | |
|       Object.defineProperty(req, 'apiId', {
 | |
|         enumerable: true
 | |
|       , configurable: false
 | |
|       , writable: false
 | |
|       , value: apiId
 | |
|       });
 | |
| 
 | |
|       if (!localCache.apis[experienceId]) {
 | |
|         localCache.apis[experienceId] = { handler: loadApiHandler(experienceId), createdAt: Date.now() };
 | |
|       }
 | |
| 
 | |
|       localCache.apis[experienceId].handler(req, res, next);
 | |
|       if (Date.now() - localCache.apis[experienceId].createdAt > (5 * 60 * 1000)) {
 | |
|         localCache.apis[experienceId] = { handler: loadApiHandler(experienceId), createdAt: Date.now() };
 | |
|       }
 | |
|     });
 | |
|   };
 | |
| };
 |