diff --git a/lib/http_forward.js b/lib/http_forward.js index bb2e9eebee8ffe6ba3e1be3eee4568931fb9888d..d49962e2194b02167a7d04eb74790f7143efeec5 100644 --- a/lib/http_forward.js +++ b/lib/http_forward.js @@ -42,13 +42,9 @@ module.exports = function(dest, req, res, cb) { preq.setHeader('content-type', req.headers['content-type']); } - // forward cookies! - if (req.cookies) { - var cookieHeader = ""; - Object.keys(req.cookies).forEach(function(key) { - cookieHeader += key + "=" + req.cookies[key] + "; "; - }); - preq.setHeader('Cookie', cookieHeader); + // forward cookies + if(req.headers['cookie']) { + preq.setHeader('Cookie', req.headers['cookie']); } // if the body has already been parsed, we'll write it diff --git a/lib/wsapi.js b/lib/wsapi.js index 963d1aff616e5f7136e4fec92cb09037bb3146db..7eb2ac33c2fc54c02155d715451bdd725c3776ee 100644 --- a/lib/wsapi.js +++ b/lib/wsapi.js @@ -85,15 +85,17 @@ exports.bcryptPassword = bcryptPassword; exports.setAuthenticatedUser = setAuthenticatedUser; exports.setup = function(options, app) { + const WSAPI_PREFIX = '/wsapi/'; - // XXX: we can and should make all of the logic below only take effect for POST requests - // to /wsapi to reduce code run for other requests (cookie parsing, etc) + // all operations that are being forwarded + var forwardedOperations = []; // If externally we're serving content over SSL we can enable things // like strict transport security and change the way cookies are set const overSSL = (config.get('scheme') == 'https'); - app.use(express.cookieParser()); + var cookieParser = express.cookieParser(); + var bodyParser = express.bodyParser(); var cookieSessionMiddleware = sessions({ secret: COOKIE_SECRET, @@ -108,14 +110,16 @@ exports.setup = function(options, app) { } }); - // cookie sessions && cache control app.use(function(req, resp, next) { + var purl = url.parse(req.url); + // cookie sessions are only applied to calls to /wsapi // as all other resources can be aggressively cached // by layers higher up based on cache control headers. // the fallout is that all code that interacts with sessions // should be under /wsapi - if (/^\/wsapi/.test(req.url)) { + if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) + { // explicitly disallow caching on all /wsapi calls (issue #294) resp.setHeader('Cache-Control', 'no-cache, max-age=0'); @@ -125,51 +129,60 @@ exports.setup = function(options, app) { if (overSSL) req.connection.proxySecure = true; - return cookieSessionMiddleware(req, resp, next); - } else { - return next(); - } - }); - - app.use(express.bodyParser()); - - // Check CSRF token early. POST requests are only allowed to - // /wsapi and they always must have a valid csrf token - app.use(function(req, resp, next) { - // only on POSTs - if (req.method == "POST") { - var denied = false; - if (!/^\/wsapi/.test(req.url)) { // post requests only allowed to /wsapi - denied = true; - logger.warn("CSRF validation failure: POST only allowed to /wsapi urls. not '" + req.url + "'"); - } - - else if (req.session === undefined) { // there must be a session - denied = true; - logger.warn("CSRF validation failure: POST calls to /wsapi require an active session"); - } + const operation = purl.pathname.substr(WSAPI_PREFIX.length); - // the session must have a csrf token - else if (typeof req.session.csrf !== 'string') { - denied = true; - logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set"); + // check to see if the api is known here, before spending more time with + // the request. + if (!wsapis.hasOwnProperty(operation) || + wsapis[operation].method.toLowerCase() !== req.method.toLowerCase()) + { + return httputils.badRequest(resp, "no such api"); } - // and the token must match what is sent in the post body - else if (req.body.csrf != req.session.csrf) { - denied = true; - // if any of these things are false, then we'll block the request - logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf); + // if this request is to be forwarded, we will not perform request validation, + // cookie parsing, nor body parsing - leaving that up to the process we're forwarding + // to. + if (-1 !== forwardedOperations.indexOf(operation)) { + return next(); + } else { + // this is not a forwarded operation, perform full parsing and validation + return cookieParser(req, resp, function() { + bodyParser(req, resp, function() { + cookieSessionMiddleware(req, resp, function() { + // only on POSTs + if (req.method === "POST") { + var denied = false; + + if (req.session === undefined) { // there must be a session + denied = true; + logger.warn("CSRF validation failure: POST calls to /wsapi require an active session"); + } + + // the session must have a csrf token + else if (typeof req.session.csrf !== 'string') { + denied = true; + logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set"); + } + + // and the token must match what is sent in the post body + else if (req.body.csrf != req.session.csrf) { + denied = true; + // if any of these things are false, then we'll block the request + logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf); + } + + if (denied) return httputils.badRequest(resp, "CSRF violation"); + } + return next(); + }); + }); + }); } - - if (denied) return httputils.badRequest(resp, "CSRF violation"); - + } else { + return next(); } - return next(); }); - const WSAPI_PREFIX = '/wsapi/'; - // load all of the APIs supported by this process var wsapis = { }; @@ -197,16 +210,9 @@ exports.setup = function(options, app) { wsapis[operation] = api; - // set up the argument validator - if (api.args) { - if (!Array.isArray(api.args)) throw "exports.args must be an array of strings"; - wsapis[operation].validate = validate(api.args); - } else { - wsapis[operation].validate = function(req,res,next) { next(); }; - } - // forward writes if options.forward_writes is defined if (options.forward_writes && wsapis[operation].writes_db) { + forwardedOperations.push(operation); var forward_url = options.forward_writes + "wsapi/" + operation; wsapis[operation].process = function(req, res) { forward(forward_url, req, res, function(err) { @@ -217,8 +223,29 @@ exports.setup = function(options, app) { } }); }; + + // XXX: disable validation on forwarded requests + // (we cannot perform this validation because we don't parse cookies + // nor post bodies on forwarded requests) + // + // at some point we'll want to improve our cookie parser and + // fully validate forwarded requests both at the intermediate + // hop (webhead) AND final destination (secure webhead) + + delete api.args; // deleting args will cause arg validation to be skipped + + api.authed = false; // authed=false will prevent us from checking auth status } + // set up the argument validator + if (api.args) { + if (!Array.isArray(api.args)) throw "exports.args must be an array of strings"; + wsapis[operation].validate = validate(api.args); + } else { + wsapis[operation].validate = function(req,res,next) { next(); }; + } + + } catch(e) { var msg = "error registering " + operation + " api: " + e; logger.error(msg); @@ -247,22 +274,19 @@ exports.setup = function(options, app) { if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) { const operation = purl.pathname.substr(WSAPI_PREFIX.length); - if (wsapis.hasOwnProperty(operation) && - wsapis[operation].method.toLowerCase() === req.method.toLowerCase()) { - // does the request require authentication? - if (wsapis[operation].authed && !isAuthed(req)) { - return httputils.badRequest(resp, "requires authentication"); - } + // at this point, we *know* 'operation' is valid API, give checks performed + // above - // validate the arguments of the request - wsapis[operation].validate(req, resp, function() { - wsapis[operation].process(req, resp); - }); - } else { - return httputils.badRequest(resp, "no such api"); + // does the request require authentication? + if (wsapis[operation].authed && !isAuthed(req)) { + return httputils.badRequest(resp, "requires authentication"); } + // validate the arguments of the request + wsapis[operation].validate(req, resp, function() { + wsapis[operation].process(req, resp); + }); } else { next(); }