Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/handlers/HttpRequestHandler.ts
1030 views
1
import * as http from 'http';
2
import Log, { hasBeenLoggedSymbol } from '@secret-agent/commons/Logger';
3
import { ClientHttp2Stream, Http2ServerRequest, Http2ServerResponse } from 'http2';
4
import { CanceledPromiseError } from '@secret-agent/commons/interfaces/IPendingWaitEvent';
5
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
6
import HeadersHandler from './HeadersHandler';
7
import MitmRequestContext from '../lib/MitmRequestContext';
8
import { parseRawHeaders } from '../lib/Utils';
9
import BaseHttpHandler from './BaseHttpHandler';
10
import HttpResponseCache from '../lib/HttpResponseCache';
11
import ResourceState from '../interfaces/ResourceState';
12
13
const { log } = Log(module);
14
15
export default class HttpRequestHandler extends BaseHttpHandler {
16
protected static responseCache = new HttpResponseCache();
17
18
constructor(
19
request: Pick<
20
IMitmRequestContext,
21
'requestSession' | 'isSSL' | 'clientToProxyRequest' | 'proxyToClientResponse'
22
>,
23
) {
24
super(request, false, HttpRequestHandler.responseCache);
25
this.context.setState(ResourceState.ClientToProxyRequest);
26
27
// register error listeners first
28
this.bindErrorListeners();
29
}
30
31
public async onRequest(): Promise<void> {
32
const { clientToProxyRequest } = this.context;
33
34
try {
35
clientToProxyRequest.pause();
36
37
const proxyToServerRequest = await this.createProxyToServerRequest();
38
if (!proxyToServerRequest) return;
39
40
type HttpServerResponse = [
41
response: IMitmRequestContext['serverToProxyResponse'],
42
flags?: number,
43
rawHeaders?: string[],
44
];
45
const responsePromise = new Promise<HttpServerResponse>(resolve => {
46
this.context.eventSubscriber.once(proxyToServerRequest, 'response', (r, flags, headers) =>
47
resolve([r, flags, headers]),
48
);
49
});
50
51
clientToProxyRequest.resume();
52
53
const socketClosedPromise = this.context.proxyToServerMitmSocket.closedPromise.promise;
54
55
// now write request - make sure socket doesn't exit before writing
56
const didWriteRequest = await Promise.race([this.writeRequest(), socketClosedPromise]);
57
58
if (didWriteRequest instanceof Date) {
59
throw new Error('Socket closed before request written');
60
}
61
62
// wait for response and make sure socket doesn't exit before writing
63
const response = await Promise.race([responsePromise, socketClosedPromise]);
64
65
if (response instanceof Date) {
66
throw new Error('Socket closed before response received');
67
}
68
await this.onResponse(...response);
69
} catch (err) {
70
this.onError('ClientToProxy.HandlerError', err);
71
}
72
}
73
74
protected async onResponse(
75
response: IMitmRequestContext['serverToProxyResponse'],
76
flags?: number,
77
rawHeaders?: string[],
78
): Promise<void> {
79
const context = this.context;
80
81
context.setState(ResourceState.ServerToProxyOnResponse);
82
83
if (response instanceof http.IncomingMessage) {
84
MitmRequestContext.readHttp1Response(context, response);
85
} else {
86
MitmRequestContext.readHttp2Response(
87
context,
88
context.proxyToServerRequest as ClientHttp2Stream,
89
response[':status'],
90
rawHeaders,
91
);
92
}
93
// wait for MitmRequestContext to read this
94
context.eventSubscriber.on(
95
context.serverToProxyResponse,
96
'error',
97
this.onError.bind(this, 'ServerToProxy.ResponseError'),
98
);
99
100
try {
101
context.cacheHandler.onResponseHeaders();
102
} catch (err) {
103
return this.onError('ServerToProxy.ResponseHeadersHandlerError', err);
104
}
105
106
/////// WRITE CLIENT RESPONSE //////////////////
107
108
if (!context.proxyToClientResponse) {
109
log.warn('Error.NoProxyToClientResponse', {
110
sessionId: context.requestSession.sessionId,
111
});
112
context.setState(ResourceState.PrematurelyClosed);
113
return;
114
}
115
116
await context.requestSession.willSendResponse(context);
117
118
try {
119
this.writeResponseHead();
120
} catch (err) {
121
return this.onError('ServerToProxyToClient.WriteResponseHeadError', err);
122
}
123
124
try {
125
await this.writeResponse();
126
} catch (err) {
127
return this.onError('ServerToProxyToClient.ReadWriteResponseError', err);
128
}
129
context.setState(ResourceState.End);
130
this.cleanup();
131
}
132
133
protected onError(kind: string, error: Error): void {
134
const isCanceled = error instanceof CanceledPromiseError;
135
136
const url = this.context.url.href;
137
const { method, requestSession, proxyToClientResponse } = this.context;
138
// already cleaned up
139
if (requestSession === null || proxyToClientResponse === null) return;
140
141
const sessionId = requestSession.sessionId;
142
143
this.context.setState(ResourceState.Error);
144
requestSession.emit('http-error', {
145
request: MitmRequestContext.toEmittedResource(this.context),
146
error,
147
});
148
149
let status = 504;
150
if (isCanceled) {
151
status = 444;
152
}
153
if (!isCanceled && !requestSession.isClosing && !error[hasBeenLoggedSymbol]) {
154
log.info(`MitmHttpRequest.${kind}`, {
155
sessionId,
156
request: `${method}: ${url}`,
157
error,
158
});
159
}
160
161
try {
162
if (!proxyToClientResponse.headersSent) {
163
proxyToClientResponse.writeHead(status);
164
proxyToClientResponse.end(error.stack);
165
} else if (!proxyToClientResponse.finished) {
166
proxyToClientResponse.end();
167
}
168
} catch (e) {
169
// drown errors
170
}
171
this.cleanup();
172
}
173
174
private bindErrorListeners(): void {
175
const { clientToProxyRequest, proxyToClientResponse } = this.context;
176
this.context.eventSubscriber.on(
177
clientToProxyRequest,
178
'error',
179
this.onError.bind(this, 'ClientToProxy.RequestError'),
180
);
181
this.context.eventSubscriber.on(
182
proxyToClientResponse,
183
'error',
184
this.onError.bind(this, 'ProxyToClient.ResponseError'),
185
);
186
187
if (clientToProxyRequest instanceof Http2ServerRequest) {
188
const stream = clientToProxyRequest.stream;
189
this.bindHttp2ErrorListeners('ClientToProxy', stream, stream.session);
190
}
191
192
if (proxyToClientResponse instanceof Http2ServerResponse) {
193
const stream = proxyToClientResponse.stream;
194
this.bindHttp2ErrorListeners('ProxyToClient', stream, stream.session);
195
}
196
}
197
198
private async writeRequest(): Promise<void> {
199
this.context.setState(ResourceState.WriteProxyToServerRequestBody);
200
const { proxyToServerRequest, clientToProxyRequest } = this.context;
201
202
const onWriteError = (error): void => {
203
if (error) {
204
this.onError('ProxyToServer.WriteError', error);
205
}
206
};
207
208
const data: Buffer[] = [];
209
for await (const chunk of clientToProxyRequest) {
210
data.push(chunk);
211
proxyToServerRequest.write(chunk, onWriteError);
212
}
213
214
HeadersHandler.sendRequestTrailers(this.context);
215
await new Promise(resolve => proxyToServerRequest.end(resolve));
216
this.context.requestPostData = Buffer.concat(data);
217
}
218
219
private writeResponseHead(): void {
220
const context = this.context;
221
const { serverToProxyResponse, proxyToClientResponse, requestSession } = context;
222
223
proxyToClientResponse.statusCode = context.status;
224
// write individually so we properly write header-lists
225
for (const [key, value] of Object.entries(context.responseHeaders)) {
226
try {
227
proxyToClientResponse.setHeader(key, value);
228
} catch (error) {
229
log.info(`MitmHttpRequest.writeResponseHeadError`, {
230
sessionId: requestSession.sessionId,
231
request: `${context.method}: ${context.url.href}`,
232
error,
233
header: [key, value],
234
});
235
}
236
}
237
238
this.context.eventSubscriber.once(serverToProxyResponse, 'trailers', headers => {
239
context.responseTrailers = headers;
240
});
241
242
proxyToClientResponse.writeHead(proxyToClientResponse.statusCode);
243
}
244
245
private async writeResponse(): Promise<void> {
246
const context = this.context;
247
const { serverToProxyResponse, proxyToClientResponse } = context;
248
249
context.setState(ResourceState.WriteProxyToClientResponseBody);
250
251
for await (const chunk of serverToProxyResponse) {
252
const data = context.cacheHandler.onResponseData(chunk as Buffer);
253
this.safeWriteToClient(data);
254
}
255
256
if (context.cacheHandler.shouldServeCachedData) {
257
this.safeWriteToClient(context.cacheHandler.cacheData);
258
}
259
260
if (serverToProxyResponse instanceof http.IncomingMessage) {
261
context.responseTrailers = parseRawHeaders(serverToProxyResponse.rawTrailers);
262
}
263
if (context.responseTrailers) {
264
proxyToClientResponse.addTrailers(context.responseTrailers);
265
}
266
await new Promise<void>(resolve => proxyToClientResponse.end(resolve));
267
268
context.requestSession.requestAgent.freeSocket(context);
269
context.cacheHandler.onResponseEnd();
270
271
// wait for browser request id before resolving
272
await context.browserHasRequested;
273
context.requestSession.emit('response', MitmRequestContext.toEmittedResource(context));
274
}
275
276
private safeWriteToClient(data: Buffer): void {
277
if (!data || this.isClientConnectionDestroyed()) return;
278
279
this.context.proxyToClientResponse.write(data, error => {
280
if (error && !this.isClientConnectionDestroyed())
281
this.onError('ServerToProxy.WriteResponseError', error);
282
});
283
}
284
285
private isClientConnectionDestroyed(): boolean {
286
const proxyToClientResponse = this.context.proxyToClientResponse;
287
return (
288
(proxyToClientResponse as Http2ServerResponse).stream?.destroyed ||
289
proxyToClientResponse.socket?.destroyed ||
290
proxyToClientResponse.connection?.destroyed
291
);
292
}
293
294
public static async onRequest(
295
request: Pick<
296
IMitmRequestContext,
297
'requestSession' | 'isSSL' | 'clientToProxyRequest' | 'proxyToClientResponse'
298
>,
299
): Promise<void> {
300
const handler = new HttpRequestHandler(request);
301
await handler.onRequest();
302
}
303
}
304
305