From a429e489776342cc0d360578fd2a7d1e74be8327 Mon Sep 17 00:00:00 2001 From: aj Date: Wed, 30 Aug 2017 17:47:31 +0000 Subject: [PATCH] enable assets subdomain with cookies --- lib/apis.js | 1265 +++++++++++++++++++++++++++++-------------------- lib/main.js | 26 +- lib/oauth3.js | 49 +- lib/worker.js | 47 +- 4 files changed, 864 insertions(+), 523 deletions(-) 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