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