Path: blob/main/mitm/handlers/HttpRequestHandler.ts
1030 views
import * as http from 'http';1import Log, { hasBeenLoggedSymbol } from '@secret-agent/commons/Logger';2import { ClientHttp2Stream, Http2ServerRequest, Http2ServerResponse } from 'http2';3import { CanceledPromiseError } from '@secret-agent/commons/interfaces/IPendingWaitEvent';4import IMitmRequestContext from '../interfaces/IMitmRequestContext';5import HeadersHandler from './HeadersHandler';6import MitmRequestContext from '../lib/MitmRequestContext';7import { parseRawHeaders } from '../lib/Utils';8import BaseHttpHandler from './BaseHttpHandler';9import HttpResponseCache from '../lib/HttpResponseCache';10import ResourceState from '../interfaces/ResourceState';1112const { log } = Log(module);1314export default class HttpRequestHandler extends BaseHttpHandler {15protected static responseCache = new HttpResponseCache();1617constructor(18request: Pick<19IMitmRequestContext,20'requestSession' | 'isSSL' | 'clientToProxyRequest' | 'proxyToClientResponse'21>,22) {23super(request, false, HttpRequestHandler.responseCache);24this.context.setState(ResourceState.ClientToProxyRequest);2526// register error listeners first27this.bindErrorListeners();28}2930public async onRequest(): Promise<void> {31const { clientToProxyRequest } = this.context;3233try {34clientToProxyRequest.pause();3536const proxyToServerRequest = await this.createProxyToServerRequest();37if (!proxyToServerRequest) return;3839type HttpServerResponse = [40response: IMitmRequestContext['serverToProxyResponse'],41flags?: number,42rawHeaders?: string[],43];44const responsePromise = new Promise<HttpServerResponse>(resolve => {45this.context.eventSubscriber.once(proxyToServerRequest, 'response', (r, flags, headers) =>46resolve([r, flags, headers]),47);48});4950clientToProxyRequest.resume();5152const socketClosedPromise = this.context.proxyToServerMitmSocket.closedPromise.promise;5354// now write request - make sure socket doesn't exit before writing55const didWriteRequest = await Promise.race([this.writeRequest(), socketClosedPromise]);5657if (didWriteRequest instanceof Date) {58throw new Error('Socket closed before request written');59}6061// wait for response and make sure socket doesn't exit before writing62const response = await Promise.race([responsePromise, socketClosedPromise]);6364if (response instanceof Date) {65throw new Error('Socket closed before response received');66}67await this.onResponse(...response);68} catch (err) {69this.onError('ClientToProxy.HandlerError', err);70}71}7273protected async onResponse(74response: IMitmRequestContext['serverToProxyResponse'],75flags?: number,76rawHeaders?: string[],77): Promise<void> {78const context = this.context;7980context.setState(ResourceState.ServerToProxyOnResponse);8182if (response instanceof http.IncomingMessage) {83MitmRequestContext.readHttp1Response(context, response);84} else {85MitmRequestContext.readHttp2Response(86context,87context.proxyToServerRequest as ClientHttp2Stream,88response[':status'],89rawHeaders,90);91}92// wait for MitmRequestContext to read this93context.eventSubscriber.on(94context.serverToProxyResponse,95'error',96this.onError.bind(this, 'ServerToProxy.ResponseError'),97);9899try {100context.cacheHandler.onResponseHeaders();101} catch (err) {102return this.onError('ServerToProxy.ResponseHeadersHandlerError', err);103}104105/////// WRITE CLIENT RESPONSE //////////////////106107if (!context.proxyToClientResponse) {108log.warn('Error.NoProxyToClientResponse', {109sessionId: context.requestSession.sessionId,110});111context.setState(ResourceState.PrematurelyClosed);112return;113}114115await context.requestSession.willSendResponse(context);116117try {118this.writeResponseHead();119} catch (err) {120return this.onError('ServerToProxyToClient.WriteResponseHeadError', err);121}122123try {124await this.writeResponse();125} catch (err) {126return this.onError('ServerToProxyToClient.ReadWriteResponseError', err);127}128context.setState(ResourceState.End);129this.cleanup();130}131132protected onError(kind: string, error: Error): void {133const isCanceled = error instanceof CanceledPromiseError;134135const url = this.context.url.href;136const { method, requestSession, proxyToClientResponse } = this.context;137// already cleaned up138if (requestSession === null || proxyToClientResponse === null) return;139140const sessionId = requestSession.sessionId;141142this.context.setState(ResourceState.Error);143requestSession.emit('http-error', {144request: MitmRequestContext.toEmittedResource(this.context),145error,146});147148let status = 504;149if (isCanceled) {150status = 444;151}152if (!isCanceled && !requestSession.isClosing && !error[hasBeenLoggedSymbol]) {153log.info(`MitmHttpRequest.${kind}`, {154sessionId,155request: `${method}: ${url}`,156error,157});158}159160try {161if (!proxyToClientResponse.headersSent) {162proxyToClientResponse.writeHead(status);163proxyToClientResponse.end(error.stack);164} else if (!proxyToClientResponse.finished) {165proxyToClientResponse.end();166}167} catch (e) {168// drown errors169}170this.cleanup();171}172173private bindErrorListeners(): void {174const { clientToProxyRequest, proxyToClientResponse } = this.context;175this.context.eventSubscriber.on(176clientToProxyRequest,177'error',178this.onError.bind(this, 'ClientToProxy.RequestError'),179);180this.context.eventSubscriber.on(181proxyToClientResponse,182'error',183this.onError.bind(this, 'ProxyToClient.ResponseError'),184);185186if (clientToProxyRequest instanceof Http2ServerRequest) {187const stream = clientToProxyRequest.stream;188this.bindHttp2ErrorListeners('ClientToProxy', stream, stream.session);189}190191if (proxyToClientResponse instanceof Http2ServerResponse) {192const stream = proxyToClientResponse.stream;193this.bindHttp2ErrorListeners('ProxyToClient', stream, stream.session);194}195}196197private async writeRequest(): Promise<void> {198this.context.setState(ResourceState.WriteProxyToServerRequestBody);199const { proxyToServerRequest, clientToProxyRequest } = this.context;200201const onWriteError = (error): void => {202if (error) {203this.onError('ProxyToServer.WriteError', error);204}205};206207const data: Buffer[] = [];208for await (const chunk of clientToProxyRequest) {209data.push(chunk);210proxyToServerRequest.write(chunk, onWriteError);211}212213HeadersHandler.sendRequestTrailers(this.context);214await new Promise(resolve => proxyToServerRequest.end(resolve));215this.context.requestPostData = Buffer.concat(data);216}217218private writeResponseHead(): void {219const context = this.context;220const { serverToProxyResponse, proxyToClientResponse, requestSession } = context;221222proxyToClientResponse.statusCode = context.status;223// write individually so we properly write header-lists224for (const [key, value] of Object.entries(context.responseHeaders)) {225try {226proxyToClientResponse.setHeader(key, value);227} catch (error) {228log.info(`MitmHttpRequest.writeResponseHeadError`, {229sessionId: requestSession.sessionId,230request: `${context.method}: ${context.url.href}`,231error,232header: [key, value],233});234}235}236237this.context.eventSubscriber.once(serverToProxyResponse, 'trailers', headers => {238context.responseTrailers = headers;239});240241proxyToClientResponse.writeHead(proxyToClientResponse.statusCode);242}243244private async writeResponse(): Promise<void> {245const context = this.context;246const { serverToProxyResponse, proxyToClientResponse } = context;247248context.setState(ResourceState.WriteProxyToClientResponseBody);249250for await (const chunk of serverToProxyResponse) {251const data = context.cacheHandler.onResponseData(chunk as Buffer);252this.safeWriteToClient(data);253}254255if (context.cacheHandler.shouldServeCachedData) {256this.safeWriteToClient(context.cacheHandler.cacheData);257}258259if (serverToProxyResponse instanceof http.IncomingMessage) {260context.responseTrailers = parseRawHeaders(serverToProxyResponse.rawTrailers);261}262if (context.responseTrailers) {263proxyToClientResponse.addTrailers(context.responseTrailers);264}265await new Promise<void>(resolve => proxyToClientResponse.end(resolve));266267context.requestSession.requestAgent.freeSocket(context);268context.cacheHandler.onResponseEnd();269270// wait for browser request id before resolving271await context.browserHasRequested;272context.requestSession.emit('response', MitmRequestContext.toEmittedResource(context));273}274275private safeWriteToClient(data: Buffer): void {276if (!data || this.isClientConnectionDestroyed()) return;277278this.context.proxyToClientResponse.write(data, error => {279if (error && !this.isClientConnectionDestroyed())280this.onError('ServerToProxy.WriteResponseError', error);281});282}283284private isClientConnectionDestroyed(): boolean {285const proxyToClientResponse = this.context.proxyToClientResponse;286return (287(proxyToClientResponse as Http2ServerResponse).stream?.destroyed ||288proxyToClientResponse.socket?.destroyed ||289proxyToClientResponse.connection?.destroyed290);291}292293public static async onRequest(294request: Pick<295IMitmRequestContext,296'requestSession' | 'isSSL' | 'clientToProxyRequest' | 'proxyToClientResponse'297>,298): Promise<void> {299const handler = new HttpRequestHandler(request);300await handler.onRequest();301}302}303304305