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; }; diff --git a/server/middleware/apiProxy.js b/server/middleware/apiProxy.js index dbab9dc7..666d0125 100644 --- a/server/middleware/apiProxy.js +++ b/server/middleware/apiProxy.js @@ -7,6 +7,49 @@ var _ = require('underscore'); */ var separator = '/-/'; +function getApiCookiePrefix(apiName) { + return (apiName || 'default') + separator; +} + +function extractCookieName(cookieString) { + return cookieString.split('=').shift(); +} + +function extractCookieValue(cookieString) { + return cookieString.split('=').pop(); +} + +function extractCookiesForApi(req, apiName) { + var rawCookieString = req.get('cookie') || '', + apiCookies = rawCookieString.split('; '), + apiCookiePrefix = getApiCookiePrefix(apiName); + + return apiCookies + .filter(function (cookie) { + var cookieName = extractCookieName(cookie); + return cookieName.indexOf(apiCookiePrefix) === 0; + }) + .map(function (cookie) { + return decodeURIComponent(extractCookieValue(cookie)); + }); +}; + +function encodeApiCookies(responseFromApi, apiName) { + var apiCookiePrefix = getApiCookiePrefix(apiName), + setCookieHeaders = []; + + if (responseFromApi.headers && responseFromApi.headers['set-cookie']) { + setCookieHeaders = responseFromApi.headers['set-cookie']; + } + + return setCookieHeaders.map(function (setCookieHeader) { + var cookieName = apiCookiePrefix + extractCookieName(setCookieHeader), + cookieValue = encodeURIComponent(setCookieHeader); + + return cookieName + '=' + cookieValue; + }); +} + /** * Middleware handler for intercepting API routes. */ @@ -20,14 +63,15 @@ 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 }, function(err, response, body) { if (err) return next(err); - // Pass through statusCode. res.status(response.statusCode); + 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 499340f7..245ece2c 100644 --- a/test/server/middleware/apiProxy.test.js +++ b/test/server/middleware/apiProxy.test.js @@ -7,32 +7,91 @@ describe('apiProxy', function() { describe('middleware', function () { - var dataAdater, proxy, responseToClient; + var dataAdapter, proxy, responseToClient, req; beforeEach(function () { - dataAdater = { request: sinon.stub() }, - proxy = apiProxy(dataAdater), - responseToClient = { status: sinon.spy(), json: sinon.spy() }; + dataAdapter = { request: sinon.stub() }; + proxy = apiProxy(dataAdapter); + responseToClient = { status: sinon.spy(), json: sinon.spy(), setHeader: sinon.spy() }; + req = { path: '/', get: sinon.stub() }; }); it('should pass through the status code', function () { - dataAdater.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' }; - dataAdater.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=/' + ], + 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', expecetedEncodedCookies) + }); + + it('should pass through prefixed cookies', function () { + 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', 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/-/'; + proxy(req, responseToClient); + 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; path=/']}}); + }); + + it('should pass through the cookies from client to the default api host', function () { + 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; path=/']}}) + }); + }); }); describe('getApiPath', function() {