'use strict'; module.exports.create = function (xconfx, apiFactories, apiDeps) { var PromiseA = apiDeps.Promise; var mkdirpAsync = PromiseA.promisify(require('mkdirp')); var request = PromiseA.promisify(require('request')); //var express = require('express'); var express = require('express-lazy'); var fs = PromiseA.promisifyAll(require('fs')); var path = require('path'); var localCache = { rests: {}, pkgs: {}, assets: {} }; 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(req, 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; } 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; } if (clientUrih === ('api.' + xconfx.setupDomain) && -1 !== ['org.oauth3.consumer', 'azp@oauth3.org', 'oauth3.org'].indexOf(pkgId)) { // fallthrough return true; } 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('[walnut@daplie.com] accountIdx:', accountIdx); //console.log('[walnut@daplie.com] ppid:', ppid); next(); }); }); rejectableRequest(req, res, promise, "[walnut@daplie.com] attach account by id"); } function accountRequired(req, res, next) { // if this already has auth, great if (req.oauth3.ppid && req.oauth3.accountIdx) { 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")) , "[walnut@daplie.com] 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, "[walnut@daplie.com] required account (not /public)"); } function grantsRequired(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(); }; } 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 = grantsRequired; function getSitePackageStoreProp(otherPkgId) { var restPath = path.join(myConf.restPath, otherPkgId); var apiPath = path.join(myConf.apiPath, otherPkgId); 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, otherPkgId, dir); } function attachOauth3(req, res, next) { return getSitePackageStoreProp('issuer@oauth3.org').then(function (Models) { return require('./oauth3').attachOauth3(Models, req, res, next); }); } myApp.use('/', 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'); } // Use getSiteCapability('email@daplie.com') instead Object.defineProperty(req, 'getSiteMailer' /*deprecated*/, { 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, 'getSitePackageStore', { enumerable: true , configurable: false , writable: false , value: getSitePackageStoreProp }); 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 settingsPromise = PromiseA.resolve(); function manageSiteSettings(section) { var submanager; var manager = { set: function (section, value) { if ('email@daplie.com' === section) { section = 'mailgun.org'; } settingsPromise = settingsPromise.then(function () { return manager.get().then(function () { siteConfig[section] = value; var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih); return mkdirpAsync(siteConfigPath).then(function () { return fs.writeFileAsync(path.join(siteConfigPath, 'config.json'), JSON.stringify(siteConfig), 'utf8'); }); }); }); return settingsPromise; } , get: function (section) { if ('email@daplie.com' === section) { section = 'mailgun.org'; } settingsPromise = settingsPromise.then(function () { return getSiteConfig(clientUrih).then(function (_siteConfig) { siteConfig = _siteConfig; return PromiseA.resolve((_siteConfig || {})[section]); }); }); return settingsPromise; } }; submanager = manager; if (section) { submanager = { set: function (value) { return manager.set(section, value); } , get: function () { return manager.get(section); } }; } return apiDeps.Promise.resolve(submanager); } var caps = { // // Capabilities for APIs // 'settings.site@daplie.com': manageSiteSettings , '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; try { pkgRestAssets = require(path.join(pkgPath, 'assets.js')); } catch(e) { return PromiseA.reject(e); } 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 = grantsRequired; function otherGetSitePackageStoreProp(otherPkgId) { var restPath = path.join(myConf.restPath, otherPkgId); var apiPath = path.join(myConf.apiPath, otherPkgId); 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, otherPkgId, dir); } myApp.use('/', function cookieAttachOauth3(req, res, next) { return otherGetSitePackageStoreProp('issuer@oauth3.org').then(function (Models) { return require('./oauth3').cookieOauth3(Models, req, res, next); }); }); myApp.use('/', function (req, res, next) { console.log('########################################### session ###############################'); console.log('req.url', req.url); console.log('req.oauth3', req.oauth3); next(); }); function otherAttachOauth3(req, res, next) { return otherGetSitePackageStoreProp('issuer@oauth3.org').then(function (Models) { return require('./oauth3').attachOauth3(Models, req, res, next); }); } myApp.post('/assets/issuer@oauth3.org/session', otherAttachOauth3, 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) { 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); 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; }, function (err) { console.error('[lib/api.js] no assets handler:'); console.error(err); return stuff; }); }); }); }); } // 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] after cors'); } // Canonical client names // example.com should use api.example.com/api for all requests // sub.example.com/api should resolve to sub.example.com // example.com/subapp/api should resolve to example.com#subapp // sub.example.com/subapp/api should resolve to sub.example.com#subapp var appUri = req.hostname.replace(/^(api|assets)\./, '') + req.url.replace(/\/(api|assets)\/.*/, '/').replace(/\/$/, ''); var clientUrih = appUri.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(/\/$/, ''); //var clientAssetsUri = req.hostname.replace(/^(api|assets)\./, 'api.') + 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|assets)\//, '').replace(/^\//, '').replace(/\/.*/, ''); var now = Date.now(); var hasBeenHandled = false; Object.defineProperty(req, 'clientUrl', { enumerable: true , configurable: false , writable: false , value: (req.headers.referer || ('https://' + appUri)).replace(/\/$/, '').replace(/\?.*/, '') }); Object.defineProperty(req, 'apiUrlPrefix', { enumerable: true , configurable: false , 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' /*deprecated*/, { enumerable: true , configurable: false , writable: false , value: clientUrih }); Object.defineProperty(req, 'clientApiUri', { enumerable: true , configurable: false , writable: false , value: clientApiUri }); Object.defineProperty(req, 'clientAssetsUri', { enumerable: true , configurable: false , writable: false , value: clientAssetsUri }); Object.defineProperty(req, 'apiId', { enumerable: true , configurable: false , writable: false , value: pkgId }); 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(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]) { if (handleWithHandler()) { return; } hasBeenHandled = true; if (now - localCache.rests[pkgId].createdAt > staleAfter) { localCache.rests[pkgId] = null; localCache.assets[pkgId] = null; } } if (!localCache.rests[pkgId]) { //return doesThisPkgExist //if (xconfx.debug) { console.log('[api.js] before rest handler'); } return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) { if (!myHandler) { //if (xconfx.debug) { console.log('[api.js] not configured'); } notConfigured(req, res); return; } localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now }; localCache.assets[pkgId] = { handler: myHandler.assetsHandler, createdAt: now }; if (!hasBeenHandled) { if (handleWithHandler()) { return; } } }); } }); rejectableRequest(req, res, promise, "[walnut@daplie.com] load api package"); }); }; };