Path: blob/main/mitm/handlers/Http2PushPromiseHandler.ts
1030 views
import * as http2 from 'http2';1import { ClientHttp2Stream, ServerHttp2Stream } from 'http2';2import Log, { hasBeenLoggedSymbol } from '@secret-agent/commons/Logger';3import { CanceledPromiseError } from '@secret-agent/commons/interfaces/IPendingWaitEvent';4import { IBoundLog } from '@secret-agent/interfaces/ILog';5import IMitmRequestContext from '../interfaces/IMitmRequestContext';6import MitmRequestContext from '../lib/MitmRequestContext';7import BlockHandler from './BlockHandler';8import HeadersHandler from './HeadersHandler';9import ResourceState from '../interfaces/ResourceState';10import RequestSession from './RequestSession';1112const { log } = Log(module);1314export default class Http2PushPromiseHandler {15private readonly context: IMitmRequestContext;16private onResponseHeadersPromise: Promise<void>;17private logger: IBoundLog;18private get session(): RequestSession {19return this.context.requestSession;20}2122constructor(23readonly parentContext: IMitmRequestContext,24serverPushStream: http2.ClientHttp2Stream,25readonly requestHeaders: http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader,26flags: number,27rawHeaders: string[],28) {29const session = parentContext.requestSession;30const sessionId = session.sessionId;31this.logger = log.createChild(module, {32sessionId,33});34this.logger.info('Http2Client.pushReceived', { requestHeaders, flags });35this.context = MitmRequestContext.createFromHttp2Push(parentContext, rawHeaders);36this.context.eventSubscriber.on(serverPushStream, 'error', error => {37this.logger.warn('Http2.ProxyToServer.PushStreamError', {38error,39});40});41this.context.serverToProxyResponse = serverPushStream;42this.session.trackResourceRedirects(this.context);43this.context.setState(ResourceState.ServerToProxyPush);44this.session.emit('request', MitmRequestContext.toEmittedResource(this.context));45}4647public async onRequest(): Promise<void> {48const pushContext = this.context;49const parentContext = this.parentContext;50const session = this.session;51const serverPushStream = this.context.serverToProxyResponse as http2.ClientHttp2Stream;5253if (BlockHandler.shouldBlockRequest(pushContext)) {54await pushContext.browserHasRequested;55session.emit('response', MitmRequestContext.toEmittedResource(pushContext));56pushContext.setState(ResourceState.Blocked);57return serverPushStream.close(http2.constants.NGHTTP2_CANCEL);58}5960HeadersHandler.cleanPushHeaders(pushContext);61this.onResponseHeadersPromise = new Promise<void>(resolve => {62this.context.eventSubscriber.once(63serverPushStream,64'push',65(responseHeaders, responseFlags, responseRawHeaders) => {66MitmRequestContext.readHttp2Response(67pushContext,68serverPushStream,69responseHeaders[':status'],70responseRawHeaders,71);72resolve();73},74);75});7677if (serverPushStream.destroyed) {78pushContext.setState(ResourceState.PrematurelyClosed);79return;80}8182const clientToProxyRequest = parentContext.clientToProxyRequest as http2.Http2ServerRequest;83pushContext.setState(ResourceState.ProxyToClientPush);84try {85clientToProxyRequest.stream.pushStream(86pushContext.requestHeaders,87this.onClientPushPromiseCreated.bind(this),88);89} catch (error) {90this.logger.warn('Http2.ClientToProxy.CreatePushStreamError', {91error,92});93}94}9596private async onClientPushPromiseCreated(97createPushStreamError: Error,98proxyToClientPushStream: ServerHttp2Stream,99): Promise<void> {100this.context.setState(ResourceState.ProxyToClientPushResponse);101const serverToProxyPushStream = this.context.serverToProxyResponse as ClientHttp2Stream;102const cache = this.context.cacheHandler;103const session = this.context.requestSession;104105if (createPushStreamError) {106this.logger.warn('Http2.ClientToProxy.PushStreamError', {107error: createPushStreamError,108});109return;110}111this.context.eventSubscriber.on(proxyToClientPushStream, 'error', pushError => {112this.logger.warn('Http2.ClientToProxy.PushStreamError', {113error: pushError,114});115});116117this.context.eventSubscriber.on(serverToProxyPushStream, 'headers', additional => {118if (!proxyToClientPushStream.destroyed) proxyToClientPushStream.additionalHeaders(additional);119});120121let trailers: http2.IncomingHttpHeaders;122this.context.eventSubscriber.once(serverToProxyPushStream, 'trailers', trailerHeaders => {123trailers = trailerHeaders;124});125126await this.onResponseHeadersPromise;127if (proxyToClientPushStream.destroyed || serverToProxyPushStream.destroyed) {128return;129}130cache.onHttp2PushStream();131132try {133if (cache.shouldServeCachedData) {134if (!proxyToClientPushStream.destroyed) {135proxyToClientPushStream.write(cache.cacheData, err => {136if (err) this.onHttp2PushError('Http2PushProxyToClient.CacheWriteError', err);137});138}139if (!serverToProxyPushStream.destroyed) {140serverToProxyPushStream.close(http2.constants.NGHTTP2_REFUSED_STREAM);141}142} else {143proxyToClientPushStream.respond(this.context.responseHeaders, { waitForTrailers: true });144145this.context.eventSubscriber.on(proxyToClientPushStream, 'wantTrailers', (): void => {146this.context.responseTrailers = trailers;147if (trailers) proxyToClientPushStream.sendTrailers(this.context.responseTrailers ?? {});148else proxyToClientPushStream.close();149});150151this.context.setState(ResourceState.ServerToProxyPushResponse);152for await (const chunk of serverToProxyPushStream) {153if (proxyToClientPushStream.destroyed || serverToProxyPushStream.destroyed) return;154cache.onResponseData(chunk);155proxyToClientPushStream.write(chunk, err => {156if (err) this.onHttp2PushError('Http2PushProxyToClient.WriteError', err);157});158}159if (!serverToProxyPushStream.destroyed) serverToProxyPushStream.end();160}161162if (!proxyToClientPushStream.destroyed) proxyToClientPushStream.end();163cache.onResponseEnd();164165await HeadersHandler.determineResourceType(this.context);166await this.context.browserHasRequested;167session.emit('response', MitmRequestContext.toEmittedResource(this.context));168} catch (writeError) {169this.onHttp2PushError('Http2PushProxyToClient.UnhandledError', writeError);170if (!proxyToClientPushStream.destroyed) proxyToClientPushStream.destroy();171} finally {172this.cleanupEventListeners();173}174}175176private cleanupEventListeners(): void {177this.context.eventSubscriber.close('error');178}179180private onHttp2PushError(kind: string, error: Error): void {181const isCanceled = error instanceof CanceledPromiseError;182183this.context.setState(ResourceState.Error);184this.session?.emit('http-error', {185request: MitmRequestContext.toEmittedResource(this.context),186error,187});188189if (!isCanceled && !this.session?.isClosing && !error[hasBeenLoggedSymbol]) {190this.logger.info(`MitmHttpRequest.${kind}`, {191request: `H2PUSH: ${this.context.url.href}`,192error,193});194}195this.cleanupEventListeners();196}197}198199200