From 02009ee4afa3a4d264ebce7e656241164afe4eee Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 16 May 2018 18:45:48 +0700 Subject: [PATCH 1/7] Add userResHeadersDecorator --- README.md | 4 ++++ app/steps/decorateUserResHeaders.js | 30 +++++++++++++++++++++++++++++ index.js | 2 ++ lib/resolveOptions.js | 1 + types.d.ts | 1 + 5 files changed, 38 insertions(+) create mode 100644 app/steps/decorateUserResHeaders.js diff --git a/README.md b/README.md index e01a58f..b06bec1 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,10 @@ instance, but this is not a reliable interface. I expect to close this exploit in a future release, while providing an additional hook for mutating the userRes before sending. +#### userResHeadersDecorator (supports Promise) + +You can modify the proxy's headers before sending it to the client. + ##### gzip responses If your proxy response is gzipped, this program will automatically unzip diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js new file mode 100644 index 0000000..1793336 --- /dev/null +++ b/app/steps/decorateUserResHeaders.js @@ -0,0 +1,30 @@ +'use strict'; + +function decorateProxyResHeaders(container) { + var ctx = container.user.ctx; + var rsp = container.proxy.res; + var modifierFn = container.options.userResHeadersDecorator; + if (!modifierFn || ctx.status !== 504) { + return Promise.resolve(container); + } + Promise.all( + Object.keys(rsp.headers) + .map(function (header) { + return modifierFn(header, rsp.headers[header]).then(function (value) { + return { + headerName: header, + newValue: value + } + }); + }) + ) + .then(function (modHeaders) { + values.forEach(function (modHeaders) { + ctx.set(modHeaders.headerName, modHeaders.newValue); + }); + resolve(container); + }) +} + +module.exports = decorateProxyResHeaders; + diff --git a/index.js b/index.js index fad3d33..90329ec 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ var copyProxyResHeadersToUserRes = require('./app/steps/copyProxyResHeadersToUse var decorateProxyReqBody = require('./app/steps/decorateProxyReqBody'); var decorateProxyReqOpts = require('./app/steps/decorateProxyReqOpts'); var decorateUserRes = require('./app/steps/decorateUserRes'); +var decorateUserResHeaders = require('./app/steps/decorateUserResHeaders'); var prepareProxyReq = require('./app/steps/prepareProxyReq'); var resolveProxyHost = require('./app/steps/resolveProxyHost'); var resolveProxyReqPath = require('./app/steps/resolveProxyReqPath'); @@ -33,6 +34,7 @@ module.exports = function proxy(host, userOptions) { .then(prepareProxyReq) .then(sendProxyRequest) .then(copyProxyResHeadersToUserRes) + .then(decorateUserResHeaders) .then(decorateUserRes) .then(sendUserRes) .then(next); diff --git a/lib/resolveOptions.js b/lib/resolveOptions.js index 8e994be..edc4a8b 100644 --- a/lib/resolveOptions.js +++ b/lib/resolveOptions.js @@ -19,6 +19,7 @@ function resolveOptions(options) { proxyReqOptDecorator: options.proxyReqOptDecorator, proxyReqBodyDecorator: options.proxyReqBodyDecorator, userResDecorator: options.userResDecorator, + userResHeadersDecorator: options.userResHeadersDecorator, filter: options.filter || defaultFilter, // For backwards compatability, we default to legacy behavior for newly added settings. parseReqBody: isUnset(options.parseReqBody) ? true : options.parseReqBody, diff --git a/types.d.ts b/types.d.ts index 96ef91e..018dda0 100644 --- a/types.d.ts +++ b/types.d.ts @@ -20,6 +20,7 @@ declare namespace koaHttpProxy { proxyReqOptDecorator?(proxyReqOpts: IRequestOption, ctx: koa.Context): IRequestOption | Promise, proxyReqPathResolver?(ctx: koa.Context): string | Promise, userResDecorator?(proxyRes: http.IncomingMessage, proxyResData: string | Buffer, ctx: koa.Context): string | Buffer | Promise | Promise, + userResHeadersDecorator?(headerName: string, headerValue: string): Promise, } export interface IRequestOption { From c9d0325d743bc25ec82b5c46de69e5dc3b74aa6c Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 16 May 2018 19:19:10 +0700 Subject: [PATCH 2/7] fix bugs --- app/steps/decorateUserResHeaders.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js index 1793336..4a518a8 100644 --- a/app/steps/decorateUserResHeaders.js +++ b/app/steps/decorateUserResHeaders.js @@ -4,10 +4,10 @@ function decorateProxyResHeaders(container) { var ctx = container.user.ctx; var rsp = container.proxy.res; var modifierFn = container.options.userResHeadersDecorator; - if (!modifierFn || ctx.status !== 504) { + if (!modifierFn || ctx.status === 504) { return Promise.resolve(container); } - Promise.all( + return Promise.all( Object.keys(rsp.headers) .map(function (header) { return modifierFn(header, rsp.headers[header]).then(function (value) { @@ -19,10 +19,10 @@ function decorateProxyResHeaders(container) { }) ) .then(function (modHeaders) { - values.forEach(function (modHeaders) { + modHeaders.forEach(function (modHeaders) { ctx.set(modHeaders.headerName, modHeaders.newValue); }); - resolve(container); + return container; }) } From 1ca6212d75b68db738a0c7767ba993fcd381664e Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 17 May 2018 09:44:29 +0700 Subject: [PATCH 3/7] add strippedHeaders option --- README.md | 13 +++++++++++++ app/steps/copyProxyResHeadersToUserRes.js | 2 ++ app/steps/decorateUserResHeaders.js | 2 +- lib/resolveOptions.js | 1 + types.d.ts | 3 ++- 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b06bec1..be34617 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,19 @@ app.use(proxy('www.google.com', { })); ``` + +#### strippedHeaders + +Headers to remove from proxy response. + +```js +app.use(proxy('www.google.com', { + strippedHeaders: [ + 'set-cookie' + ] +})); +``` + #### preserveReqSession Pass the session along to the proxied request diff --git a/app/steps/copyProxyResHeadersToUserRes.js b/app/steps/copyProxyResHeadersToUserRes.js index 15e1c03..4a4f58b 100644 --- a/app/steps/copyProxyResHeadersToUserRes.js +++ b/app/steps/copyProxyResHeadersToUserRes.js @@ -4,10 +4,12 @@ function copyProxyResHeadersToUserRes(container) { return new Promise(function(resolve) { var ctx = container.user.ctx; var rsp = container.proxy.res; + var strippedHeaders = container.options.strippedHeaders || []; if (!ctx.headerSent && ctx.status !== 504) { ctx.status = rsp.statusCode; Object.keys(rsp.headers) + .filter(function(item) { return strippedHeaders.indexOf(item) < 0; }) .filter(function(item) { return item !== 'transfer-encoding'; }) .forEach(function(item) { ctx.set(item, rsp.headers[item]); diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js index 4a518a8..0f132f8 100644 --- a/app/steps/decorateUserResHeaders.js +++ b/app/steps/decorateUserResHeaders.js @@ -10,7 +10,7 @@ function decorateProxyResHeaders(container) { return Promise.all( Object.keys(rsp.headers) .map(function (header) { - return modifierFn(header, rsp.headers[header]).then(function (value) { + return Promise.resolve(modifierFn(header, rsp.headers[header])).then(function (value) { return { headerName: header, newValue: value diff --git a/lib/resolveOptions.js b/lib/resolveOptions.js index edc4a8b..670b261 100644 --- a/lib/resolveOptions.js +++ b/lib/resolveOptions.js @@ -25,6 +25,7 @@ function resolveOptions(options) { parseReqBody: isUnset(options.parseReqBody) ? true : options.parseReqBody, reqBodyEncoding: resolveBodyEncoding(options.reqBodyEncoding), headers: options.headers, + strippedHeaders: options.strippedHeaders, preserveReqSession: options.preserveReqSession, https: options.https, port: options.port, diff --git a/types.d.ts b/types.d.ts index 018dda0..2f408a8 100644 --- a/types.d.ts +++ b/types.d.ts @@ -6,6 +6,7 @@ declare function koaHttpProxy(host: string, options: koaHttpProxy.IOptions): koa declare namespace koaHttpProxy { export interface IOptions { headers?: { [key: string]: any }, + strippedHeaders?: [string], https?: boolean, limit?: string, parseReqBody?: boolean, @@ -20,7 +21,7 @@ declare namespace koaHttpProxy { proxyReqOptDecorator?(proxyReqOpts: IRequestOption, ctx: koa.Context): IRequestOption | Promise, proxyReqPathResolver?(ctx: koa.Context): string | Promise, userResDecorator?(proxyRes: http.IncomingMessage, proxyResData: string | Buffer, ctx: koa.Context): string | Buffer | Promise | Promise, - userResHeadersDecorator?(headerName: string, headerValue: string): Promise, + userResHeadersDecorator?(headerName: string, headerValue: string): Promise | string, } export interface IRequestOption { From 2e6233553cbae1a9375fd529d29aef7a116565a6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 17 May 2018 10:08:36 +0700 Subject: [PATCH 4/7] fixed JSlint errors --- app/steps/decorateUserResHeaders.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js index 0f132f8..f2bb4dc 100644 --- a/app/steps/decorateUserResHeaders.js +++ b/app/steps/decorateUserResHeaders.js @@ -14,7 +14,7 @@ function decorateProxyResHeaders(container) { return { headerName: header, newValue: value - } + }; }); }) ) @@ -23,7 +23,7 @@ function decorateProxyResHeaders(container) { ctx.set(modHeaders.headerName, modHeaders.newValue); }); return container; - }) + }); } module.exports = decorateProxyResHeaders; From 739d23c79bed8fd051e6af9f91e3598261e69ffb Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 17 May 2018 10:11:32 +0700 Subject: [PATCH 5/7] Fixed code styling errors --- app/steps/decorateUserResHeaders.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js index f2bb4dc..cc51d37 100644 --- a/app/steps/decorateUserResHeaders.js +++ b/app/steps/decorateUserResHeaders.js @@ -9,8 +9,8 @@ function decorateProxyResHeaders(container) { } return Promise.all( Object.keys(rsp.headers) - .map(function (header) { - return Promise.resolve(modifierFn(header, rsp.headers[header])).then(function (value) { + .map(function(header) { + return Promise.resolve(modifierFn(header, rsp.headers[header])).then(function(value) { return { headerName: header, newValue: value @@ -18,8 +18,8 @@ function decorateProxyResHeaders(container) { }); }) ) - .then(function (modHeaders) { - modHeaders.forEach(function (modHeaders) { + .then(function(modHeaders) { + modHeaders.forEach(function(modHeaders) { ctx.set(modHeaders.headerName, modHeaders.newValue); }); return container; From 7c7164ea5996067dabc13964044825f5fbc85bb5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 21 May 2018 15:25:20 +0700 Subject: [PATCH 6/7] Decorate all headers at once --- app/steps/copyProxyResHeadersToUserRes.js | 18 ++++++++------ app/steps/decorateUserResHeaders.js | 30 ----------------------- index.js | 2 -- types.d.ts | 2 +- 4 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 app/steps/decorateUserResHeaders.js diff --git a/app/steps/copyProxyResHeadersToUserRes.js b/app/steps/copyProxyResHeadersToUserRes.js index 4a4f58b..8020bbf 100644 --- a/app/steps/copyProxyResHeadersToUserRes.js +++ b/app/steps/copyProxyResHeadersToUserRes.js @@ -5,20 +5,22 @@ function copyProxyResHeadersToUserRes(container) { var ctx = container.user.ctx; var rsp = container.proxy.res; var strippedHeaders = container.options.strippedHeaders || []; + var userResHeadersDecorator = container.options.userResHeadersDecorator || function(headers) { return headers; }; - if (!ctx.headerSent && ctx.status !== 504) { - ctx.status = rsp.statusCode; - Object.keys(rsp.headers) + if (ctx.headerSent || ctx.status === 504) { + return resolve(container); + } + ctx.status = rsp.statusCode; + Promise.resolve(userResHeadersDecorator(rsp.headers)).then(function(decoratedHeaders) { + Object.keys(decoratedHeaders) .filter(function(item) { return strippedHeaders.indexOf(item) < 0; }) .filter(function(item) { return item !== 'transfer-encoding'; }) .forEach(function(item) { - ctx.set(item, rsp.headers[item]); + ctx.set(item, decoratedHeaders[item]); }); - } - - resolve(container); + resolve(container); + }); }); } module.exports = copyProxyResHeadersToUserRes; - diff --git a/app/steps/decorateUserResHeaders.js b/app/steps/decorateUserResHeaders.js deleted file mode 100644 index cc51d37..0000000 --- a/app/steps/decorateUserResHeaders.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -function decorateProxyResHeaders(container) { - var ctx = container.user.ctx; - var rsp = container.proxy.res; - var modifierFn = container.options.userResHeadersDecorator; - if (!modifierFn || ctx.status === 504) { - return Promise.resolve(container); - } - return Promise.all( - Object.keys(rsp.headers) - .map(function(header) { - return Promise.resolve(modifierFn(header, rsp.headers[header])).then(function(value) { - return { - headerName: header, - newValue: value - }; - }); - }) - ) - .then(function(modHeaders) { - modHeaders.forEach(function(modHeaders) { - ctx.set(modHeaders.headerName, modHeaders.newValue); - }); - return container; - }); -} - -module.exports = decorateProxyResHeaders; - diff --git a/index.js b/index.js index 90329ec..fad3d33 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,6 @@ var copyProxyResHeadersToUserRes = require('./app/steps/copyProxyResHeadersToUse var decorateProxyReqBody = require('./app/steps/decorateProxyReqBody'); var decorateProxyReqOpts = require('./app/steps/decorateProxyReqOpts'); var decorateUserRes = require('./app/steps/decorateUserRes'); -var decorateUserResHeaders = require('./app/steps/decorateUserResHeaders'); var prepareProxyReq = require('./app/steps/prepareProxyReq'); var resolveProxyHost = require('./app/steps/resolveProxyHost'); var resolveProxyReqPath = require('./app/steps/resolveProxyReqPath'); @@ -34,7 +33,6 @@ module.exports = function proxy(host, userOptions) { .then(prepareProxyReq) .then(sendProxyRequest) .then(copyProxyResHeadersToUserRes) - .then(decorateUserResHeaders) .then(decorateUserRes) .then(sendUserRes) .then(next); diff --git a/types.d.ts b/types.d.ts index 2f408a8..049cb7c 100644 --- a/types.d.ts +++ b/types.d.ts @@ -21,7 +21,7 @@ declare namespace koaHttpProxy { proxyReqOptDecorator?(proxyReqOpts: IRequestOption, ctx: koa.Context): IRequestOption | Promise, proxyReqPathResolver?(ctx: koa.Context): string | Promise, userResDecorator?(proxyRes: http.IncomingMessage, proxyResData: string | Buffer, ctx: koa.Context): string | Buffer | Promise | Promise, - userResHeadersDecorator?(headerName: string, headerValue: string): Promise | string, + userResHeadersDecorator?(headers: {[key: string]: string}): Promise<{[key: string]: string}> | {[key: string]: string}, } export interface IRequestOption { From 39896a25a8e98be857cf4023ae99e89e9e4284a7 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 30 May 2018 12:09:37 +0700 Subject: [PATCH 7/7] write tests --- test/userResDecorator.js | 105 ++++++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/test/userResDecorator.js b/test/userResDecorator.js index c5bf568..1ba0a30 100644 --- a/test/userResDecorator.js +++ b/test/userResDecorator.js @@ -5,11 +5,39 @@ var Koa = require('koa'); var agent = require('supertest').agent; var proxy = require('../'); -describe('userResDecorator', function() { +function proxyTarget(port) { + var other = new Koa(); + other.use(function(ctx, next) { + if (ctx.request.url !== '/json') { + return next(); + } + ctx.set('content-type', 'app,lication/json'); + ctx.body = JSON.stringify({foo: 'bar'}); + }); + other.use(function(ctx) { + ctx.status = 200; + ctx.set('x-wombat-alliance', 'mammels'); + ctx.set('x-custom-header', 'something'); + ctx.body = 'Success'; + }); + return other.listen(port); +} + +describe.only('userResDecorator', function() { + var other; + + beforeEach(function() { + other = proxyTarget(8080); + }); + + afterEach(function() { + other.close(); + }); it('has access to original response', function(done) { var app = new Koa(); - app.use(proxy('httpbin.org', { + app.use(proxy('http://localhost', { + port: 8080, userResDecorator: function(proxyRes, proxyResData) { assert(proxyRes.connection); assert(proxyRes.socket); @@ -24,7 +52,8 @@ describe('userResDecorator', function() { it('works with promises', function(done) { var app = new Koa(); - app.use(proxy('httpbin.org', { + app.use(proxy('http://localhost', { + port: 8080, userResDecorator: function(proxyRes, proxyResData) { return new Promise(function(resolve) { proxyResData.funkyMessage = 'oi io oo ii'; @@ -36,7 +65,7 @@ describe('userResDecorator', function() { })); agent(app.callback()) - .get('/ip') + .get('/') .end(function(err, res) { if (err) { return done(err); } @@ -48,7 +77,8 @@ describe('userResDecorator', function() { it('can modify the response data', function(done) { var app = new Koa(); - app.use(proxy('httpbin.org', { + app.use(proxy('http://localhost', { + port: 8080, userResDecorator: function(proxyRes, proxyResData) { proxyResData = JSON.parse(proxyResData.toString('utf8')); proxyResData.intercepted = true; @@ -57,7 +87,7 @@ describe('userResDecorator', function() { })); agent(app.callback()) - .get('/ip') + .get('/json') .end(function(err, res) { if (err) { return done(err); } @@ -66,14 +96,54 @@ describe('userResDecorator', function() { }); }); + it('can filter response headers', function(done) { + var proxiedApp = new Koa(); + var app = new Koa(); + var p1Done, p2Done; + var p1 = new Promise(function(resolve) { p1Done = resolve; }); + var p2 = new Promise(function(resolve) { p2Done = resolve; }); + app.use(proxy('http://localhost', { + port: 8080 + })); + proxiedApp.use(proxy('http://localhost', { + port: 8080, + strippedHeaders: ['x-wombat-alliance', 'x-custom-header'] + })); - it('can modify the response headers, [deviant case, supported by pass-by-reference atm]', function(done) { + agent(app.callback()) + .get('/') + .end(function(err, res) { + if (err) { return done(err); } + assert(typeof res.headers['x-custom-header'] === 'string'); + assert(typeof res.headers['x-wombat-alliance'] === 'string'); + p1Done(); + }); + + agent(proxiedApp.callback()) + .get('/') + .end(function(err, res) { + if (err) { return done(err); } + assert(typeof res.headers['x-custom-header'] !== 'string'); + assert(typeof res.headers['x-wombat-alliance'] !== 'string'); + p2Done(); + }); + + Promise.all([p1, p2]).then(function() { done(); }); + }); + + it('can modify the response headers', function(done) { var app = new Koa(); - app.use(proxy('httpbin.org', { - userResDecorator: function(rsp, data, ctx) { - ctx.set('x-wombat-alliance', 'mammels'); - ctx.set('content-type', 'wiki/wiki'); - return data; + app.use(proxy('http://localhost', { + port: 8080, + userResHeadersDecorator: function(headers) { + var newHeaders = Object.keys(headers) + .reduce(function(result, key) { + result[key] = headers[key]; + return result; + }, {}); + newHeaders['x-transaction-id'] = '12345'; + newHeaders['x-entity-id'] = 'abcdef'; + return newHeaders; } })); @@ -81,24 +151,25 @@ describe('userResDecorator', function() { .get('/ip') .end(function(err, res) { if (err) { return done(err); } - assert(res.headers['content-type'] === 'wiki/wiki'); - assert(res.headers['x-wombat-alliance'] === 'mammels'); + assert(res.headers['x-transaction-id'] === '12345'); + assert(res.headers['x-entity-id'] === 'abcdef'); done(); }); }); it('can mutuate an html response', function(done) { var app = new Koa(); - app.use(proxy('httpbin.org', { + app.use(proxy('http://localhost', { + port: 8080, userResDecorator: function(rsp, data) { - data = data.toString().replace('Oh', 'Hey'); + data = data.toString().replace('Success', 'Hey'); assert(data !== ''); return data; } })); agent(app.callback()) - .get('/html') + .get('/') .end(function(err, res) { if (err) { return done(err); } assert(res.text.indexOf('Hey') > -1);