From 01797d01ef94cba4e40f772cd4162cacdda05351 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Tue, 17 Dec 2013 11:43:16 +0100 Subject: [PATCH 1/7] fix typo --- test/server/middleware/apiProxy.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/server/middleware/apiProxy.test.js b/test/server/middleware/apiProxy.test.js index 499340f7..d4964bf8 100644 --- a/test/server/middleware/apiProxy.test.js +++ b/test/server/middleware/apiProxy.test.js @@ -7,16 +7,16 @@ describe('apiProxy', function() { describe('middleware', function () { - var dataAdater, proxy, responseToClient; + var dataAdapter, proxy, responseToClient; beforeEach(function () { - dataAdater = { request: sinon.stub() }, - proxy = apiProxy(dataAdater), + dataAdapter = { request: sinon.stub() }, + proxy = apiProxy(dataAdapter), responseToClient = { status: sinon.spy(), json: sinon.spy() }; }); it('should pass through the status code', function () { - dataAdater.request.yields(null, {status: 200}, {}); + dataAdapter.request.yields(null, {status: 200}, {}); proxy({ path: '/' }, responseToClient); @@ -25,7 +25,7 @@ describe('apiProxy', function() { it('should pass through the body', function () { var body = { what: 'ever' }; - dataAdater.request.yields(null, {status: 200}, body); + dataAdapter.request.yields(null, {status: 200}, body); proxy({ path: '/' }, responseToClient); From 082450e43cdd4a4cff3b965dcfd8c674a8eb5313 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Tue, 17 Dec 2013 13:38:26 +0100 Subject: [PATCH 2/7] replace comma with semicolon --- test/server/middleware/apiProxy.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/server/middleware/apiProxy.test.js b/test/server/middleware/apiProxy.test.js index d4964bf8..0a72a9a1 100644 --- a/test/server/middleware/apiProxy.test.js +++ b/test/server/middleware/apiProxy.test.js @@ -10,8 +10,8 @@ describe('apiProxy', function() { var dataAdapter, proxy, responseToClient; beforeEach(function () { - dataAdapter = { request: sinon.stub() }, - proxy = apiProxy(dataAdapter), + dataAdapter = { request: sinon.stub() }; + proxy = apiProxy(dataAdapter); responseToClient = { status: sinon.spy(), json: sinon.spy() }; }); From 1c9ca8639346176866d48c23f92f3fefe1310314 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Tue, 17 Dec 2013 14:08:47 +0100 Subject: [PATCH 3/7] implement cookie forwarding --- server/middleware/apiProxy.js | 28 ++++++++++++ test/server/middleware/apiProxy.test.js | 59 ++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/server/middleware/apiProxy.js b/server/middleware/apiProxy.js index dbab9dc7..44e1ed54 100644 --- a/server/middleware/apiProxy.js +++ b/server/middleware/apiProxy.js @@ -7,6 +7,32 @@ var _ = require('underscore'); */ var separator = '/-/'; +function getApiCookiePrefix(apiName) { + return (apiName || 'default') + separator +} + +function extractCookiesForApi(req, apiName) { + var apiCookiePrefix = getApiCookiePrefix(apiName), + incomingCookies = (req.get('cookie') || '').split('; '); + + return incomingCookies + .filter(function (cookie) { + return apiCookiePrefix === cookie.substr(0, apiCookiePrefix.length); + }) + .map(function (cookie) { + return cookie.substr(apiCookiePrefix.length); + }); +} + +function prefixSetCookieHeaderWithApiName(responseFromApi, api) { + var outgoingCookies = responseFromApi.headers['set-cookie'] || [], + apiCookiePrefix = getApiCookiePrefix(api.api); + + return outgoingCookies.map(function (cookie) { + return apiCookiePrefix + cookie; + }); +} + /** * Middleware handler for intercepting API routes. */ @@ -20,6 +46,7 @@ function apiProxy(dataAdapter) { api.path = apiProxy.getApiPath(req.path); api.api = apiProxy.getApiName(req.path); + api.headers = {cookie: extractCookiesForApi(req, api.api)}; dataAdapter.request(req, api, { convertErrorCode: false @@ -28,6 +55,7 @@ function apiProxy(dataAdapter) { // Pass through statusCode. res.status(response.statusCode); + res.setHeader('set-cookie', prefixSetCookieHeaderWithApiName(response, api)); res.json(body); }); }; diff --git a/test/server/middleware/apiProxy.test.js b/test/server/middleware/apiProxy.test.js index 0a72a9a1..75555d45 100644 --- a/test/server/middleware/apiProxy.test.js +++ b/test/server/middleware/apiProxy.test.js @@ -7,32 +7,79 @@ describe('apiProxy', function() { describe('middleware', function () { - var dataAdapter, proxy, responseToClient; + var dataAdapter, proxy, responseToClient, req; beforeEach(function () { dataAdapter = { request: sinon.stub() }; proxy = apiProxy(dataAdapter); - responseToClient = { status: sinon.spy(), json: sinon.spy() }; + responseToClient = { status: sinon.spy(), json: sinon.spy(), setHeader: sinon.spy() }; + req = { path: '/', get: sinon.stub() }; }); it('should pass through the status code', function () { - dataAdapter.request.yields(null, {status: 200}, {}); + dataAdapter.request.yields(null, {status: 200, headers: {}}, {}); - proxy({ path: '/' }, responseToClient); + proxy(req, responseToClient); responseToClient.status.should.have.been.calledOnce; }); it('should pass through the body', function () { var body = { what: 'ever' }; - dataAdapter.request.yields(null, {status: 200}, body); + dataAdapter.request.yields(null, {status: 200, headers: {}}, body); - proxy({ path: '/' }, responseToClient); + proxy(req, responseToClient); responseToClient.json.should.have.been.calledOnce; responseToClient.json.should.have.been.calledWith(body); }); + describe('cookie forwarding', function () { + it('should pass through prefixed cookies for the default api', function () { + var cookiesReturnedByApi = [ + 'FooBar=SomeCookieData; path=/', + 'BarFoo=OtherCookieData; path=/' + ]; + + dataAdapter.request.yields(null, { headers: { 'set-cookie': cookiesReturnedByApi } }); + proxy(req, responseToClient); + + responseToClient.setHeader.should.have.been.calledOnce; + responseToClient.setHeader.should.have.been.calledWith('set-cookie', ['default/-/FooBar=SomeCookieData; path=/', 'default/-/BarFoo=OtherCookieData; path=/']) + }); + + it('should pass through prefixed cookies', function () { + var cookiesReturnedByApi = [ 'FooBar=SomeCookieData; path=/' ]; + + dataAdapter.request.yields(null, { headers: { 'set-cookie': cookiesReturnedByApi } }); + req.path = '/apiName/-/'; + proxy(req, responseToClient); + + responseToClient.setHeader.should.have.been.calledOnce; + responseToClient.setHeader.should.have.been.calledWith('set-cookie', ['apiName/-/FooBar=SomeCookieData; path=/']) + }); + + it('should pass through the cookies from client to the correct api host', function () { + req.path = '/apiName/-/'; + req.get.withArgs('cookie').returns('apiName/-/FooBar=SomeCookieData; otherApi/-/BarFoo=OtherCookieData'); + proxy(req, responseToClient); + + dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['FooBar=SomeCookieData']}}); + + req.path = '/otherApi/-/'; + proxy(req, responseToClient); + + dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['BarFoo=OtherCookieData']}}); + }); + + it('should pass through the cookies from client to the default api host', function () { + req.get.withArgs('cookie').returns('default/-/FooBar=SomeCookieData'); + proxy(req, responseToClient); + + dataAdapter.request.should.have.been.calledOnce; + dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['FooBar=SomeCookieData']}}) + }); + }); }); describe('getApiPath', function() { From dda05f11edc49ac24d44082bab2edfc1e9fb5340 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Tue, 17 Dec 2013 14:09:41 +0100 Subject: [PATCH 4/7] remove comment, stating the obvious --- server/middleware/apiProxy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/middleware/apiProxy.js b/server/middleware/apiProxy.js index 44e1ed54..470cf123 100644 --- a/server/middleware/apiProxy.js +++ b/server/middleware/apiProxy.js @@ -53,7 +53,6 @@ function apiProxy(dataAdapter) { }, function(err, response, body) { if (err) return next(err); - // Pass through statusCode. res.status(response.statusCode); res.setHeader('set-cookie', prefixSetCookieHeaderWithApiName(response, api)); res.json(body); From 9b25af7cd10e523648a0058499a67ae8b8099d60 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Wed, 18 Dec 2013 11:09:25 +0100 Subject: [PATCH 5/7] disable global cookie jar to prevent cookie sharing between different clients --- server/data_adapter/rest_adapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/data_adapter/rest_adapter.js b/server/data_adapter/rest_adapter.js index 492966f7..64d0d222 100644 --- a/server/data_adapter/rest_adapter.js +++ b/server/data_adapter/rest_adapter.js @@ -142,6 +142,8 @@ RestAdapter.prototype.apiDefaults = function(api, req) { delete api.body; } + // disable global cookie jar + api.jar = false; return api; }; From 00c927eed38ddb8a063385f16865392154a0eaa5 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Wed, 18 Dec 2013 11:11:16 +0100 Subject: [PATCH 6/7] encode the whole set-cookie header retrieved from the api This prevents the cookie options form the api host are getting set on the client cookie. --- server/middleware/apiProxy.js | 39 ++++++++++++++++--------- test/server/middleware/apiProxy.test.js | 32 +++++++++++++------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/server/middleware/apiProxy.js b/server/middleware/apiProxy.js index 470cf123..cc38ea24 100644 --- a/server/middleware/apiProxy.js +++ b/server/middleware/apiProxy.js @@ -8,28 +8,41 @@ var _ = require('underscore'); var separator = '/-/'; function getApiCookiePrefix(apiName) { - return (apiName || 'default') + separator + return (apiName || 'default') + separator; +} + +function extractCookieName(cookieString) { + return cookieString.split('=').shift(); +} + +function extractCookieValue(cookieString) { + return cookieString.split('=').pop(); } function extractCookiesForApi(req, apiName) { - var apiCookiePrefix = getApiCookiePrefix(apiName), - incomingCookies = (req.get('cookie') || '').split('; '); + var rawCookieString = req.get('cookie') || '', + apiCookies = rawCookieString.split('; '), + apiCookiePrefix = getApiCookiePrefix(apiName); - return incomingCookies + return apiCookies .filter(function (cookie) { - return apiCookiePrefix === cookie.substr(0, apiCookiePrefix.length); + var cookieName = extractCookieName(cookie); + return cookieName.indexOf(apiCookiePrefix) === 0; }) .map(function (cookie) { - return cookie.substr(apiCookiePrefix.length); + return decodeURIComponent(extractCookieValue(cookie)); }); -} +}; + +function encodeApiCookies(responseFromApi, apiName) { + var apiCookiePrefix = getApiCookiePrefix(apiName), + setCookieHeaders = responseFromApi.headers['set-cookie'] || []; -function prefixSetCookieHeaderWithApiName(responseFromApi, api) { - var outgoingCookies = responseFromApi.headers['set-cookie'] || [], - apiCookiePrefix = getApiCookiePrefix(api.api); + return setCookieHeaders.map(function (setCookieHeader) { + var cookieName = apiCookiePrefix + extractCookieName(setCookieHeader), + cookieValue = encodeURIComponent(setCookieHeader); - return outgoingCookies.map(function (cookie) { - return apiCookiePrefix + cookie; + return cookieName + '=' + cookieValue; }); } @@ -54,7 +67,7 @@ function apiProxy(dataAdapter) { if (err) return next(err); res.status(response.statusCode); - res.setHeader('set-cookie', prefixSetCookieHeaderWithApiName(response, api)); + res.setHeader('set-cookie', encodeApiCookies(response, api.api)); res.json(body); }); }; diff --git a/test/server/middleware/apiProxy.test.js b/test/server/middleware/apiProxy.test.js index 75555d45..245ece2c 100644 --- a/test/server/middleware/apiProxy.test.js +++ b/test/server/middleware/apiProxy.test.js @@ -39,45 +39,57 @@ describe('apiProxy', function() { var cookiesReturnedByApi = [ 'FooBar=SomeCookieData; path=/', 'BarFoo=OtherCookieData; path=/' + ], + expecetedEncodedCookies = [ + 'default/-/FooBar=' + encodeURIComponent('FooBar=SomeCookieData; path=/'), + 'default/-/BarFoo=' + encodeURIComponent('BarFoo=OtherCookieData; path=/') ]; + dataAdapter.request.yields(null, { headers: { 'set-cookie': cookiesReturnedByApi } }); proxy(req, responseToClient); responseToClient.setHeader.should.have.been.calledOnce; - responseToClient.setHeader.should.have.been.calledWith('set-cookie', ['default/-/FooBar=SomeCookieData; path=/', 'default/-/BarFoo=OtherCookieData; path=/']) + responseToClient.setHeader.should.have.been.calledWith('set-cookie', expecetedEncodedCookies) }); it('should pass through prefixed cookies', function () { - var cookiesReturnedByApi = [ 'FooBar=SomeCookieData; path=/' ]; + var cookiesReturnedByApi = [ 'FooBar=SomeCookieData; path=/' ], + expecetedEncodedCookies = [ + 'apiName/-/FooBar=' + encodeURIComponent('FooBar=SomeCookieData; path=/') + ]; dataAdapter.request.yields(null, { headers: { 'set-cookie': cookiesReturnedByApi } }); req.path = '/apiName/-/'; proxy(req, responseToClient); responseToClient.setHeader.should.have.been.calledOnce; - responseToClient.setHeader.should.have.been.calledWith('set-cookie', ['apiName/-/FooBar=SomeCookieData; path=/']) + responseToClient.setHeader.should.have.been.calledWith('set-cookie', expecetedEncodedCookies) }); it('should pass through the cookies from client to the correct api host', function () { + var encodedClientCookies = + 'apiName/-/FooBar=' + encodeURIComponent('FooBar=SomeCookieData; path=/') + + '; ' + + 'otherApi/-/BarFoo=' + encodeURIComponent('BarFoo=OtherCookieData; path=/'); + + req.get.withArgs('cookie').returns(encodedClientCookies); + req.path = '/apiName/-/'; - req.get.withArgs('cookie').returns('apiName/-/FooBar=SomeCookieData; otherApi/-/BarFoo=OtherCookieData'); proxy(req, responseToClient); - - dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['FooBar=SomeCookieData']}}); + dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['FooBar=SomeCookieData; path=/']}}); req.path = '/otherApi/-/'; proxy(req, responseToClient); - - dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['BarFoo=OtherCookieData']}}); + dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['BarFoo=OtherCookieData; path=/']}}); }); it('should pass through the cookies from client to the default api host', function () { - req.get.withArgs('cookie').returns('default/-/FooBar=SomeCookieData'); + req.get.withArgs('cookie').returns('default/-/FooBar=' + encodeURIComponent('FooBar=SomeCookieData; path=/')); proxy(req, responseToClient); dataAdapter.request.should.have.been.calledOnce; - dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['FooBar=SomeCookieData']}}) + dataAdapter.request.should.have.been.calledWithMatch(req, {headers: {cookie: ['FooBar=SomeCookieData; path=/']}}) }); }); }); From 7e3db19893d95bee5e2838936871e581ce853285 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Thu, 19 Dec 2013 09:47:40 +0100 Subject: [PATCH 7/7] ensure headers are present on the response object --- server/middleware/apiProxy.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/middleware/apiProxy.js b/server/middleware/apiProxy.js index cc38ea24..666d0125 100644 --- a/server/middleware/apiProxy.js +++ b/server/middleware/apiProxy.js @@ -36,7 +36,11 @@ function extractCookiesForApi(req, apiName) { function encodeApiCookies(responseFromApi, apiName) { var apiCookiePrefix = getApiCookiePrefix(apiName), - setCookieHeaders = responseFromApi.headers['set-cookie'] || []; + setCookieHeaders = []; + + if (responseFromApi.headers && responseFromApi.headers['set-cookie']) { + setCookieHeaders = responseFromApi.headers['set-cookie']; + } return setCookieHeaders.map(function (setCookieHeader) { var cookieName = apiCookiePrefix + extractCookieName(setCookieHeader),