Skip to content

Commit

Permalink
proxyless: onConfigureResponse, onResponse events (#2803)
Browse files Browse the repository at this point in the history
  • Loading branch information
miherlosev committed Oct 19, 2022
1 parent 778eb36 commit 87c1b71
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 132 deletions.
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { SCRIPTS } from './session/injectables';
import { acceptCrossOrigin } from './utils/http';
import getAssetPath from './utils/get-asset-path';
import RequestHookEventProvider from './request-pipeline/request-hooks/events/event-provider';
import { RequestInfo } from './request-pipeline/request-hooks/events/info';
import { RequestInfo, ResponseInfo } from './request-pipeline/request-hooks/events/info';
import BaseRequestHookEventFactory from './request-pipeline/request-hooks/events/factory/base';
import BaseRequestPipelineContext from './request-pipeline/context/base';

Expand Down Expand Up @@ -60,4 +60,5 @@ export default {
INJECTABLE_SCRIPTS: SCRIPTS,
acceptCrossOrigin,
getAssetPath,
ResponseInfo,
};
32 changes: 32 additions & 0 deletions src/request-pipeline/context/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import RequestEventNames from '../request-hooks/events/names';
import ResponseMock from '../request-hooks/response-mock';
import IncomingMessageLike from '../incoming-message-like';
import getMockResponse from '../request-hooks/response-mock/get-response';
import { PreparedResponseInfo } from '../request-hooks/events/info';
import ResponseEvent from '../../session/events/response-event';
import { OnResponseEventData } from '../../typings/context';
import ConfigureResponseEventOptions from '../../session/events/configure-response-event-options';


export default abstract class BaseRequestPipelineContext {
public requestFilterRules: RequestFilterRule[];
public requestId: string;
public reqOpts: RequestOptions;
public mock: ResponseMock;
public onResponseEventData: OnResponseEventData[] = [];

protected constructor (requestId: string) {
this.requestFilterRules = [];
Expand All @@ -35,6 +40,10 @@ export default abstract class BaseRequestPipelineContext {
this.reqOpts = eventFactory.createRequestOptions();
}

public getOnResponseEventData ({ includeBody }: { includeBody: boolean }): OnResponseEventData[] {
return this.onResponseEventData.filter(eventData => eventData.opts.includeBody === includeBody);
}

public async onRequestHookRequest (eventProvider: RequestHookEventProvider, eventFactory: BaseRequestHookEventFactory): Promise<void> {
const requestInfo = eventFactory.createRequestInfo();

Expand All @@ -54,6 +63,29 @@ export default abstract class BaseRequestPipelineContext {
});
}

public async onRequestHookConfigureResponse (eventProvider: RequestHookEventProvider, eventFactory: BaseRequestHookEventFactory): Promise<void> {
await Promise.all(this.requestFilterRules.map(async rule => {
const configureResponseEvent = eventFactory.createConfigureResponseEvent(rule);

await eventProvider.callRequestEventCallback(RequestEventNames.onConfigureResponse, rule, configureResponseEvent);

this.onResponseEventData.push({
rule: configureResponseEvent.requestFilterRule,
opts: configureResponseEvent.opts,
});
}));
}

public async onRequestHookResponse (eventProvider: RequestHookEventProvider, eventFactory: BaseRequestHookEventFactory, rule: RequestFilterRule, opts: ConfigureResponseEventOptions): Promise<ResponseEvent> {
const responseInfo = eventFactory.createResponseInfo();
const preparedResponseInfo = new PreparedResponseInfo(responseInfo, opts);
const responseEvent = new ResponseEvent(rule, preparedResponseInfo);

await eventProvider.callRequestEventCallback(RequestEventNames.onResponse, rule, responseEvent);

return responseEvent;
}

public async getMockResponse (): Promise<IncomingMessageLike> {
this.mock.setRequestOptions(this.reqOpts);

Expand Down
40 changes: 35 additions & 5 deletions src/request-pipeline/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import { Http2Response } from '../destination-request/http2';
import BaseRequestPipelineContext from './base';
import RequestPipelineRequestHookEventFactory from '../request-hooks/events/factory';
import RequestHookEventProvider from '../request-hooks/events/event-provider';
import { PassThrough } from 'stream';
import promisifyStream from '../../utils/promisify-stream';
import { toReadableStream } from '../../utils/buffer';

export interface DestInfo {
url: string;
Expand Down Expand Up @@ -110,7 +113,6 @@ export default class RequestPipelineContext extends BaseRequestPipelineContext {
isWebSocketConnectionReset = false;
contentInfo: ContentInfo;
restoringStorages: StoragesSnapshot;
onResponseEventData: OnResponseEventData[] = [];
parsedClientSyncCookie: ParsedClientSyncCookie;
isFileProtocol: boolean;
nonProcessedDestResBody: Buffer;
Expand Down Expand Up @@ -511,10 +513,6 @@ export default class RequestPipelineContext extends BaseRequestPipelineContext {
logger.proxy.onMockResponseError(this.requestFilterRules[0], this.mock.error as Error);
}

getOnResponseEventData ({ includeBody }: { includeBody: boolean }): OnResponseEventData[] {
return this.onResponseEventData.filter(eventData => eventData.opts.includeBody === includeBody);
}

resolveInjectableUrl (url: string): string {
return this.serverInfo.domain + url;
}
Expand Down Expand Up @@ -555,4 +553,36 @@ export default class RequestPipelineContext extends BaseRequestPipelineContext {
if (requestCache.shouldCache(this) && !IncomingMessageLike.isIncomingMessageLike(res))
this.temporaryCacheEntry = requestCache.create(this.reqOpts, res);
}

public async callOnResponseEventCallbackWithoutBodyForNonProcessedResource (ctx: RequestPipelineContext, onResponseEventDataWithoutBody: OnResponseEventData[]) {
await Promise.all(onResponseEventDataWithoutBody.map(async eventData => {
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
}));

ctx.destRes.pipe(ctx.res);
}

public async callOnResponseEventCallbackForMotModifiedResource (ctx: RequestPipelineContext) {
await Promise.all(ctx.onResponseEventData.map(async eventData => {
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
}));

ctx.res.end();
}

public async callOnResponseEventCallbackWithBodyForNonProcessedRequest (ctx: RequestPipelineContext, onResponseEventDataWithBody: OnResponseEventData[]) {
const destResBodyCollectorStream = new PassThrough();

ctx.destRes.pipe(destResBodyCollectorStream);

promisifyStream(destResBodyCollectorStream).then(async data => {
ctx.saveNonProcessedDestResBody(data);

await Promise.all(onResponseEventDataWithBody.map(async eventData => {
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
}));

toReadableStream(data).pipe(ctx.res);
});
}
}
6 changes: 5 additions & 1 deletion src/request-pipeline/request-hooks/events/factory/base.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { RequestInfo } from '../info';
import { RequestInfo, ResponseInfo } from '../info';
import RequestOptions from '../../../request-options';
import ConfigureResponseEvent from '../../../../session/events/configure-response-event';
import RequestFilterRule from '../../request-filter-rule';


export default abstract class BaseRequestHookEventFactory {
public abstract createRequestInfo (): RequestInfo;
public abstract createRequestOptions (): RequestOptions;
public abstract createConfigureResponseEvent (rule: RequestFilterRule): ConfigureResponseEvent;
public abstract createResponseInfo (): ResponseInfo;
}
13 changes: 12 additions & 1 deletion src/request-pipeline/request-hooks/events/factory/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import BaseRequestHookEventFactory from './base';
import { RequestInfo } from '../info';
import { RequestInfo, ResponseInfo } from '../info';
import RequestPipelineContext from '../../../context';
import RequestOptions from '../../../request-options';
import ConfigureResponseEvent from '../../../../session/events/configure-response-event';
import RequestFilterRule from '../../request-filter-rule';


export default class RequestPipelineRequestHookEventFactory extends BaseRequestHookEventFactory {
Expand All @@ -12,11 +14,20 @@ export default class RequestPipelineRequestHookEventFactory extends BaseRequestH

this._ctx = ctx;
}

public createRequestInfo (): RequestInfo {
return RequestInfo.from(this._ctx);
}

public createRequestOptions (): RequestOptions {
return RequestOptions.createFrom(this._ctx);
}

public createConfigureResponseEvent (rule: RequestFilterRule): ConfigureResponseEvent {
return new ConfigureResponseEvent(rule, this._ctx);
}

public createResponseInfo (): ResponseInfo {
return ResponseInfo.from(this._ctx);
}
}
45 changes: 14 additions & 31 deletions src/request-pipeline/stages.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import RequestPipelineContext from './context';
import logger from '../utils/logger';
import { fetchBody } from '../utils/http';
import {
callOnConfigureResponseEventForNonProcessedRequest,
callOnResponseEventCallbackForFailedSameOriginCheck,
callOnResponseEventCallbackForMotModifiedResource,
callOnResponseEventCallbackWithBodyForNonProcessedRequest,
callOnResponseEventCallbackWithoutBodyForNonProcessedResource,
callResponseEventCallbackForProcessedRequest,
error,
sendRequest,
} from './utils';
import ConfigureResponseEvent from '../session/events/configure-response-event';
import ConfigureResponseEventOptions from '../session/events/configure-response-event-options';
import RequestEventNames from './request-hooks/events/names';
import { error, sendRequest } from './utils';
import { respondOnWebSocket } from './websocket';
import { noop } from 'lodash';
import { process as processResource } from '../processing/resources';
import { connectionResetGuard } from './connection-reset-guard';
import ConfigureResponseEventOptions from '../session/events/configure-response-event-options';

const EVENT_SOURCE_REQUEST_TIMEOUT = 60 * 60 * 1000;

Expand Down Expand Up @@ -72,12 +61,11 @@ export default [

ctx.isSameOriginPolicyFailed = true;

await ctx.forEachRequestFilterRule(async rule => {
const configureResponseEvent = new ConfigureResponseEvent(rule, ctx, ConfigureResponseEventOptions.DEFAULT);
await ctx.onRequestHookConfigureResponse(ctx.session.requestHookEventProvider, ctx.eventFactory);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onConfigureResponse, rule, configureResponseEvent);
await callOnResponseEventCallbackForFailedSameOriginCheck(ctx, rule, ConfigureResponseEventOptions.DEFAULT);
});
await Promise.all(ctx.onResponseEventData.map(async eventData => {
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, ConfigureResponseEventOptions.DEFAULT);
}));

logger.proxy.onCORSFailed(ctx);
},
Expand All @@ -102,19 +90,20 @@ export default [

// NOTE: Just pipe the content body to the browser if we don't need to process it.
else {
await callOnConfigureResponseEventForNonProcessedRequest(ctx);
await ctx.onRequestHookConfigureResponse(ctx.session.requestHookEventProvider, ctx.eventFactory);

ctx.sendResponseHeaders();

if (ctx.contentInfo.isNotModified)
return await callOnResponseEventCallbackForMotModifiedResource(ctx);
return await ctx.callOnResponseEventCallbackForMotModifiedResource(ctx);

const onResponseEventDataWithBody = ctx.getOnResponseEventData({ includeBody: true });
const onResponseEventDataWithoutBody = ctx.getOnResponseEventData({ includeBody: false });

if (onResponseEventDataWithBody.length)
await callOnResponseEventCallbackWithBodyForNonProcessedRequest(ctx, onResponseEventDataWithBody);
await ctx.callOnResponseEventCallbackWithBodyForNonProcessedRequest(ctx, onResponseEventDataWithBody);
else if (onResponseEventDataWithoutBody.length)
await callOnResponseEventCallbackWithoutBodyForNonProcessedResource(ctx, onResponseEventDataWithoutBody);
await ctx.callOnResponseEventCallbackWithoutBodyForNonProcessedResource(ctx, onResponseEventDataWithoutBody);
else if (ctx.req.socket.destroyed && !ctx.isDestResReadableEnded)
ctx.destRes.destroy();
else {
Expand Down Expand Up @@ -148,19 +137,13 @@ export default [
},

async function sendProxyResponse (ctx: RequestPipelineContext) {
const configureResponseEvents = await Promise.all(ctx.requestFilterRules.map(async rule => {
const configureResponseEvent = new ConfigureResponseEvent(rule, ctx, ConfigureResponseEventOptions.DEFAULT);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onConfigureResponse, rule, configureResponseEvent);

return configureResponseEvent;
}));
await ctx.onRequestHookConfigureResponse(ctx.session.requestHookEventProvider, ctx.eventFactory);

ctx.sendResponseHeaders();

connectionResetGuard(async () => {
await Promise.all(configureResponseEvents.map(async configureResponseEvent => {
await callResponseEventCallbackForProcessedRequest(ctx, configureResponseEvent);
await Promise.all(ctx.onResponseEventData.map(async eventData => {
await ctx.onRequestHookResponse(ctx.session.requestHookEventProvider, ctx.eventFactory, eventData.rule, eventData.opts);
}));

ctx.res.write(ctx.destResBody);
Expand Down
92 changes: 0 additions & 92 deletions src/request-pipeline/utils.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
import RequestPipelineContext, { DestinationResponse } from './context';
import RequestFilterRule from './request-hooks/request-filter-rule';

import {
ResponseInfo,
PreparedResponseInfo,
} from './request-hooks/events/info';

import { OnResponseEventData } from '../typings/context';
import FileRequest from './file-request';
import DestinationRequest from './destination-request';
import promisifyStream from '../utils/promisify-stream';
import ConfigureResponseEvent from '../session/events/configure-response-event';
import ResponseEvent from '../session/events/response-event';
import RequestEventNames from './request-hooks/events/names';
import ConfigureResponseEventOptions from '../session/events/configure-response-event-options';
import { toReadableStream } from '../utils/buffer';
import { PassThrough } from 'stream';
import { getText, MESSAGE } from '../messages';
import logger from '../utils/logger';
import { getFormattedInvalidCharacters } from './http-header-parser';
Expand Down Expand Up @@ -110,80 +95,3 @@ export function error (ctx: RequestPipelineContext, err: string) {
ctx.closeWithError(500, err.toString());
}

export async function callResponseEventCallbackForProcessedRequest (ctx: RequestPipelineContext, configureResponseEvent: ConfigureResponseEvent) {
const responseInfo = ResponseInfo.from(ctx);
const preparedResponseInfo = new PreparedResponseInfo(responseInfo, configureResponseEvent.opts);
const responseEvent = new ResponseEvent(configureResponseEvent.requestFilterRule, preparedResponseInfo);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onResponse, configureResponseEvent.requestFilterRule, responseEvent);

return responseEvent;
}

export async function callOnResponseEventCallbackForFailedSameOriginCheck (ctx: RequestPipelineContext, rule: RequestFilterRule, configureOpts: ConfigureResponseEventOptions) {
const responseInfo = ResponseInfo.from(ctx);
const preparedResponseInfo = new PreparedResponseInfo(responseInfo, configureOpts);
const responseEvent = new ResponseEvent(rule, preparedResponseInfo);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onResponse, rule, responseEvent);

return responseEvent;
}

export async function callOnConfigureResponseEventForNonProcessedRequest (ctx: RequestPipelineContext) {
await ctx.forEachRequestFilterRule(async rule => {
const configureResponseEvent = new ConfigureResponseEvent(rule, ctx, ConfigureResponseEventOptions.DEFAULT);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onConfigureResponse, rule, configureResponseEvent);

ctx.onResponseEventData.push({ rule, opts: configureResponseEvent.opts });
});
}

export async function callOnResponseEventCallbackWithBodyForNonProcessedRequest (ctx: RequestPipelineContext, onResponseEventDataWithBody: OnResponseEventData[]) {
const destResBodyCollectorStream = new PassThrough();

ctx.destRes.pipe(destResBodyCollectorStream);

promisifyStream(destResBodyCollectorStream).then(async data => {
ctx.saveNonProcessedDestResBody(data);

const responseInfo = ResponseInfo.from(ctx);

await Promise.all(onResponseEventDataWithBody.map(async ({ rule, opts }) => {
const preparedResponseInfo = new PreparedResponseInfo(responseInfo, opts);
const responseEvent = new ResponseEvent(rule, preparedResponseInfo);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onResponse, rule, responseEvent);
}));

toReadableStream(data).pipe(ctx.res);
});
}

export async function callOnResponseEventCallbackWithoutBodyForNonProcessedResource (ctx: RequestPipelineContext, onResponseEventDataWithoutBody: OnResponseEventData[]) {
const responseInfo = ResponseInfo.from(ctx);

await Promise.all(onResponseEventDataWithoutBody.map(async item => {
const preparedResponseInfo = new PreparedResponseInfo(responseInfo, item.opts);
const responseEvent = new ResponseEvent(item.rule, preparedResponseInfo);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onResponse, item.rule, responseEvent);
}));

ctx.destRes.pipe(ctx.res);
}

export async function callOnResponseEventCallbackForMotModifiedResource (ctx: RequestPipelineContext) {
const responseInfo = ResponseInfo.from(ctx);

await Promise.all(ctx.onResponseEventData.map(async item => {
const preparedResponseInfo = new PreparedResponseInfo(responseInfo, item.opts);
const responseEvent = new ResponseEvent(item.rule, preparedResponseInfo);

await ctx.session.requestHookEventProvider.callRequestEventCallback(RequestEventNames.onResponse, item.rule, responseEvent);
}));

ctx.res.end();
}

2 changes: 1 addition & 1 deletion src/session/events/configure-response-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class ConfigureResponseEvent {
public opts: ConfigureResponseEventOptions;
public id: string;

constructor (requestFilterRule: RequestFilterRule, requestContext: RequestPipelineContext | null, opts: ConfigureResponseEventOptions) {
constructor (requestFilterRule: RequestFilterRule, requestContext: RequestPipelineContext | null, opts = ConfigureResponseEventOptions.DEFAULT) {
this.requestFilterRule = requestFilterRule;
this._requestContext = requestContext;
this.opts = opts;
Expand Down

0 comments on commit 87c1b71

Please sign in to comment.