Skip to content

Commit

Permalink
[RequestHook]: add methods for manipulation with response headers (Cl…
Browse files Browse the repository at this point in the history
…oses DevExpress#1657) (DevExpress#1790)

* Splited header-transforms

* ConfigureResponseEvent: `setHeader` & `removeHeader`

* test/server/request-hook-test: Added ConfigureResponseEvent `removeHeader` & `setHeader` test

* test/server/proxy-test: Added Response header modification test cafe

* Renamed `header-transforms` files and exports

* fixed test/server/proxy-test typo

* request-pipeline: splitted `callResponseEventCallbackForProcessedRequest` in order to send `onResponse` on `ctx.res.end()` callback
  • Loading branch information
NickCis authored and AndreyBelym committed Feb 28, 2019
1 parent 53a9635 commit 0c1ef2f
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 76 deletions.
59 changes: 59 additions & 0 deletions src/request-pipeline/header-transforms/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
requestTransforms,
forcedRequestTransforms,
responseTransforms,
forcedResponseTransforms
} from './transforms';

// Transformation routine
function transformHeaders (srcHeaders, ctx, transformList, forcedTransforms) {
const destHeaders = {};

const applyTransform = function (headerName, headers, transforms) {
const src = headers[headerName];
const transform = transforms[headerName];
const dest = transform ? transform(src, ctx) : src;

if (dest !== void 0)
destHeaders[headerName] = dest;
};

Object.keys(srcHeaders).forEach(headerName => applyTransform(headerName, srcHeaders, transformList));

if (forcedTransforms)
Object.keys(forcedTransforms).forEach(headerName => applyTransform(headerName, destHeaders, forcedTransforms));

return destHeaders;
}

// API
export function forRequest (ctx) {
return transformHeaders(ctx.req.headers, ctx, requestTransforms, forcedRequestTransforms);
}

export function forResponse (ctx) {
return transformHeaders(ctx.destRes.headers, ctx, responseTransforms, forcedResponseTransforms);
}

export function transformHeadersCaseToRaw (headers, rawHeaders) {
const processedHeaders = {};
const headersNames = Object.keys(headers);

for (let i = 0; i < rawHeaders.length; i += 2) {
const rawHeaderName = rawHeaders[i];
const headerName = rawHeaderName.toLowerCase();
const headerIndex = headersNames.indexOf(headerName);

if (headerIndex > -1) {
processedHeaders[rawHeaderName] = headers[headerName];
headersNames[headerIndex] = void 0;
}
}

for (const headerName of headersNames) {
if (headerName !== void 0)
processedHeaders[headerName] = headers[headerName];
}

return processedHeaders;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import XHR_HEADERS from './xhr/headers';
import AUTHORIZATION from './xhr/authorization';
import * as urlUtils from '../utils/url';
import XHR_HEADERS from '../xhr/headers';
import AUTHORIZATION from '../xhr/authorization';
import * as urlUtils from '../../utils/url';
import { parse as parseUrl, resolve as resolveUrl } from 'url';
import {
formatSyncCookie,
generateDeleteSyncCookieStr,
isOutdatedSyncCookie
} from '../utils/cookie';
} from '../../utils/cookie';

// Skipping transform
function skip () {
Expand Down Expand Up @@ -109,7 +109,7 @@ function transformRefreshHeader (src, ctx) {
}

// Request headers
const requestTransforms = Object.assign({
export const requestTransforms = Object.assign({
'host': (src, ctx) => ctx.dest.host,
'referer': (src, ctx) => ctx.dest.referer || void 0,
'origin': (src, ctx) => ctx.dest.reqOrigin || src,
Expand All @@ -127,7 +127,7 @@ const requestTransforms = Object.assign({
return obj;
}, {}));

const requestForced = {
export const forcedRequestTransforms = {
'cookie': (src, ctx) => transformCookie(ctx.session.cookies.getHeader(ctx.dest.url) || void 0, ctx),

// NOTE: All browsers except Chrome don't send the 'Origin' header in case of the same domain XHR requests.
Expand All @@ -141,7 +141,7 @@ const requestForced = {


// Response headers
const responseTransforms = {
export const responseTransforms = {
// NOTE: Disable Content Security Policy (see http://en.wikipedia.org/wiki/Content_Security_Policy).
'content-security-policy': skip,
'content-security-policy-report-only': skip,
Expand Down Expand Up @@ -200,7 +200,7 @@ const responseTransforms = {
}
};

const responseForced = {
export const forcedResponseTransforms = {
'set-cookie': (src, ctx) => {
let parsedCookies;

Expand All @@ -213,56 +213,3 @@ const responseForced = {
return [];
}
};

// Transformation routine
function transformHeaders (srcHeaders, ctx, transformList, forced) {
const destHeaders = {};

const applyTransform = function (headerName, headers, transforms) {
const src = headers[headerName];
const transform = transforms[headerName];
const dest = transform ? transform(src, ctx) : src;

if (dest !== void 0)
destHeaders[headerName] = dest;
};

Object.keys(srcHeaders).forEach(headerName => applyTransform(headerName, srcHeaders, transformList));

if (forced)
Object.keys(forced).forEach(headerName => applyTransform(headerName, destHeaders, forced));

return destHeaders;
}

// API
export function forRequest (ctx) {
return transformHeaders(ctx.req.headers, ctx, requestTransforms, requestForced);
}

export function forResponse (ctx) {
return transformHeaders(ctx.destRes.headers, ctx, responseTransforms, responseForced);
}

export function transformHeadersCaseToRaw (headers, rawHeaders) {
const processedHeaders = {};
const headersNames = Object.keys(headers);

for (let i = 0; i < rawHeaders.length; i += 2) {
const rawHeaderName = rawHeaders[i];
const headerName = rawHeaderName.toLowerCase();
const headerIndex = headersNames.indexOf(headerName);

if (headerIndex > -1) {
processedHeaders[rawHeaderName] = headers[headerName];
headersNames[headerIndex] = void 0;
}
}

for (const headerName of headersNames) {
if (headerName !== void 0)
processedHeaders[headerName] = headers[headerName];
}

return processedHeaders;
}
33 changes: 19 additions & 14 deletions src/request-pipeline/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const stages = {

if (!ctx.isKeepSameOriginPolicy()) {
ctx.requestFilterRules.forEach(rule => {
const configureResponseEvent = new ConfigureResponseEvent(rule, ConfigureResponseEventOptions.DEFAULT);
const configureResponseEvent = new ConfigureResponseEvent(ctx, rule, ConfigureResponseEventOptions.DEFAULT);

ctx.session.callRequestEventCallback(REQUEST_EVENT_NAMES.onConfigureResponse, rule, configureResponseEvent);
callOnResponseEventCallbackForFailedSameOriginCheck(ctx, rule, configureResponseEvent);
Expand All @@ -106,11 +106,9 @@ const stages = {
}
// NOTE: Just pipe the content body to the browser if we don't need to process it.
else if (!ctx.contentInfo.requireProcessing) {
sendResponseHeaders(ctx);

if (!ctx.isSpecialPage) {
ctx.requestFilterRules.forEach(rule => {
const configureResponseEvent = new ConfigureResponseEvent(rule, ConfigureResponseEventOptions.DEFAULT);
const configureResponseEvent = new ConfigureResponseEvent(ctx, rule, ConfigureResponseEventOptions.DEFAULT);

ctx.session.callRequestEventCallback(REQUEST_EVENT_NAMES.onConfigureResponse, rule, configureResponseEvent);

Expand All @@ -120,6 +118,8 @@ const stages = {
ctx.onResponseEventDataWithoutBody.push({ rule, opts: configureResponseEvent.opts });
});

sendResponseHeaders(ctx);

if (ctx.contentInfo.isNotModified)
ctx.res.end();
else
Expand All @@ -144,8 +144,10 @@ const stages = {
ctx.req.on('close', () => ctx.destRes.destroy());
}
}
else
else {
sendResponseHeaders(ctx);
ctx.res.end();
}

return;
}
Expand Down Expand Up @@ -181,12 +183,19 @@ const stages = {
},

7: function sendProxyResponse (ctx) {
const configureResponseEvents = ctx.requestFilterRules.map(rule => {
const configureResponseEvent = new ConfigureResponseEvent(ctx, rule, ConfigureResponseEventOptions.DEFAULT);

ctx.session.callRequestEventCallback(REQUEST_EVENT_NAMES.onConfigureResponse, rule, configureResponseEvent);
return configureResponseEvent;
});

sendResponseHeaders(ctx);

connectionResetGuard(() => {
ctx.res.write(ctx.destResBody);
ctx.res.end(() => {
ctx.requestFilterRules.forEach(rule => callResponseEventCallbackForProcessedRequest(ctx, rule));
configureResponseEvents.forEach(configureResponseEvent => callResponseEventCallbackForProcessedRequest(ctx, configureResponseEvent));
});
});
}
Expand Down Expand Up @@ -257,16 +266,12 @@ function isDestResBodyMalformed (ctx) {
return !ctx.destResBody || ctx.destResBody.length !== ctx.destRes.headers['content-length'];
}

function callResponseEventCallbackForProcessedRequest (ctx, rule) {
const responseInfo = requestEventInfo.createResponseInfo(ctx);
const configureResponseEvent = new ConfigureResponseEvent(rule, ConfigureResponseEventOptions.DEFAULT);

ctx.session.callRequestEventCallback(REQUEST_EVENT_NAMES.onConfigureResponse, rule, configureResponseEvent);

function callResponseEventCallbackForProcessedRequest (ctx, configureResponseEvent) {
const responseInfo = requestEventInfo.createResponseInfo(ctx);
const preparedResponseInfo = requestEventInfo.prepareEventData(responseInfo, configureResponseEvent.opts);
const responseEvent = new ResponseEvent(rule, preparedResponseInfo);
const responseEvent = new ResponseEvent(configureResponseEvent._requestFilterRule, preparedResponseInfo);

ctx.session.callRequestEventCallback(REQUEST_EVENT_NAMES.onResponse, rule, responseEvent);
ctx.session.callRequestEventCallback(REQUEST_EVENT_NAMES.onResponse, configureResponseEvent._requestFilterRule, responseEvent);
}

function callOnRequestEventCallback (ctx, rule, reqInfo) {
Expand Down
11 changes: 10 additions & 1 deletion src/session/events/configure-response-event.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export default class ConfigureResponseEvent {
constructor (requestFilterRule, opts) {
constructor (requestContext, requestFilterRule, opts) {
this._requestContext = requestContext;
this._requestFilterRule = requestFilterRule;
this.opts = opts;
}

setHeader (name, value) {
this._requestContext.destRes.headers[name.toLowerCase()] = value;
}

removeHeader (name) {
delete this._requestContext.destRes.headers[name.toLowerCase()];
}
}
24 changes: 24 additions & 0 deletions test/server/proxy-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2337,6 +2337,30 @@ describe('Proxy', () => {
});
});

it('Should allow to modify response headers', () => {
const rule = new RequestFilterRule('http://127.0.0.1:2000/page');

session.addRequestEventListeners(rule, {
onConfigureResponse: e => {
e.setHeader('My-Custom-Header', 'My Custom value');
e.removeHeader('Content-Type');
}
});

const options = {
url: proxy.openSession('http://127.0.0.1:2000/page', session),
resolveWithFullResponse: true
};

return request(options)
.then(response => {
expect(response.headers['my-custom-header']).eql('My Custom value');
expect(response.headers).to.not.have.property('content-type');

session.removeRequestEventListeners(rule);
});
});

it('Should pass `forceProxySrcForImage` option in task script', () => {
session._getPayloadScript = () => 'PayloadScript';
session._getIframePayloadScript = () => 'IframePayloadScript';
Expand Down
29 changes: 29 additions & 0 deletions test/server/request-hook-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const url = require('url');
const expect = require('chai').expect;
const ResponseMock = require('../../lib/request-pipeline/request-hooks/response-mock');
const RequestFilterRule = require('../../lib/request-pipeline/request-hooks/request-filter-rule');
const ConfigureResponseEvent = require('../../lib/session/events/configure-response-event');
const ConfigureResponseEventOptions = require('../../lib/session/events/configure-response-event-options');
const noop = require('lodash').noop;

Expand Down Expand Up @@ -231,3 +232,31 @@ it('Default configure options for onResponseEvent', () => {
expect(ConfigureResponseEventOptions.DEFAULT.includeBody).eql(false);
expect(ConfigureResponseEventOptions.DEFAULT.includeHeaders).eql(false);
});

describe('ConfigureResponseEvent', () => {
it('Remove header', () => {
const mockCtx = {
destRes: {
headers: {
'my-header': 'value'
}
}
};
const configureResponseEvent = new ConfigureResponseEvent(mockCtx);

configureResponseEvent.removeHeader('My-Header');
expect(mockCtx.destRes.headers).to.not.have.property('my-header');
});

it('Set header', () => {
const mockCtx = {
destRes: {
headers: {}
}
};
const configureResponseEvent = new ConfigureResponseEvent(mockCtx);

configureResponseEvent.setHeader('My-Header', 'value');
expect(mockCtx.destRes.headers).to.have.property('my-header').that.equals('value');
});
});

0 comments on commit 0c1ef2f

Please sign in to comment.