forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			224 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			224 lines
		
	
	
		
			7.6 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 = { rests: {}, pkgs: {} };
 | |
| 
 | |
|   // TODO xconfx.apispath
 | |
|   xconfx.restPath = path.join(__dirname, '..', '..', 'packages', 'rest');
 | |
|   xconfx.appApiGrantsPath = path.join(__dirname, '..', '..', 'packages', 'client-api-grants');
 | |
| 
 | |
|   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) {
 | |
|       console.log('sanity', 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) {
 | |
|           console.log(api, pkgId, api === pkgId);
 | |
|           return true;
 | |
|         }
 | |
|       })) {
 | |
|         return true;
 | |
|       }
 | |
|         if (clientUrih === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === pkgId) {
 | |
|           // fallthrough
 | |
|           return true;
 | |
|         } else {
 | |
|           return null;
 | |
|         }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function loadRestHelper(myConf, pkgId) {
 | |
|     var pkgPath = path.join(myConf.restPath, pkgId);
 | |
| 
 | |
|     // 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 deps = {};
 | |
|       var myApp;
 | |
| 
 | |
|       if (pkg.walnut) {
 | |
|         pkgPath = path.join(pkgPath, 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(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
 | |
|       deps.memstore = apiFactories.memstoreFactory.create(pkgId);
 | |
| 
 | |
|       console.log('DEBUG pkgPath', pkgPath);
 | |
|       myApp = express();
 | |
|       myApp.use('/', function preHandler(req, res, next) {
 | |
|         req.originalUrl = req.originalUrl || req.url;
 | |
|         // "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
 | |
|         req.url = req.url.replace(/\/api\//, '').replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/');
 | |
|         console.log('[prehandler] req.url', req.url);
 | |
|         next();
 | |
|       });
 | |
|       //
 | |
|       // TODO handle /accounts/:accountId
 | |
|       //
 | |
|       return PromiseA.resolve(require(pkgPath).create({
 | |
|         etcpath: xconfx.etcpath
 | |
|       }/*pkgConf*/, deps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
 | |
|         localCache.pkgs[pkgId] = { pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
 | |
|         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, pkgId) {
 | |
|     return PromiseA.resolve().then(function () {
 | |
|       if (!localCache.pkgs[pkgId]) {
 | |
|         return loadRestHelper(myConf, 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 () {
 | |
|       console.log('[sanity check]', req.url);
 | |
|       // 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(/#$/, '');
 | |
|       // 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, 'experienceId', {
 | |
|         enumerable: true
 | |
|       , configurable: false
 | |
|       , writable: false
 | |
|       , value: clientUrih
 | |
|       });
 | |
|       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)
 | |
|       return 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, 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);
 | |
|             }
 | |
|           });
 | |
|         }
 | |
|       });
 | |
|     });
 | |
|   };
 | |
| };
 |