Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/lib/MitmRequestContext.ts
1030 views
1
import { URL } from 'url';
2
import * as http from 'http';
3
import * as http2 from 'http2';
4
import EventSubscriber from '@secret-agent/commons/EventSubscriber';
5
import IResourceRequest from '@secret-agent/interfaces/IResourceRequest';
6
import { TLSSocket } from 'tls';
7
import MitmSocket from '@secret-agent/mitm-socket';
8
import OriginType, { isOriginType } from '@secret-agent/interfaces/OriginType';
9
import IResourceHeaders from '@secret-agent/interfaces/IResourceHeaders';
10
import IResourceResponse from '@secret-agent/interfaces/IResourceResponse';
11
import { IPuppetResourceRequest } from '@secret-agent/interfaces/IPuppetNetworkEvents';
12
import HttpResponseCache from './HttpResponseCache';
13
import HeadersHandler from '../handlers/HeadersHandler';
14
import { IRequestSessionResponseEvent } from '../handlers/RequestSession';
15
import CacheHandler from '../handlers/CacheHandler';
16
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
17
import { parseRawHeaders } from './Utils';
18
import ResourceState from '../interfaces/ResourceState';
19
20
export default class MitmRequestContext {
21
private static contextIdCounter = 0;
22
23
public static createFromPuppetResourceRequest(
24
resourceLoadDetails: IPuppetResourceRequest,
25
): IMitmRequestContext {
26
return {
27
id: (this.contextIdCounter += 1),
28
...resourceLoadDetails,
29
requestOriginalHeaders: { ...resourceLoadDetails.requestHeaders },
30
didBlockResource: !!resourceLoadDetails.browserBlockedReason,
31
cacheHandler: null,
32
clientToProxyRequest: null,
33
stateChanges: new Map<ResourceState, Date>([[ResourceState.End, new Date()]]),
34
setState() {},
35
};
36
}
37
38
public static create(
39
params: Pick<
40
IMitmRequestContext,
41
'requestSession' | 'isSSL' | 'clientToProxyRequest' | 'proxyToClientResponse' | 'isUpgrade'
42
>,
43
responseCache: HttpResponseCache,
44
): IMitmRequestContext {
45
const {
46
isSSL,
47
proxyToClientResponse,
48
clientToProxyRequest,
49
requestSession,
50
isUpgrade,
51
} = params;
52
53
const protocol = isUpgrade ? 'ws' : 'http';
54
const expectedProtocol = `${protocol}${isSSL ? 's' : ''}:`;
55
56
let url: URL;
57
if (
58
clientToProxyRequest.url.startsWith('http://') ||
59
clientToProxyRequest.url.startsWith('https://') ||
60
clientToProxyRequest.url.startsWith('ws://') ||
61
clientToProxyRequest.url.startsWith('wss://')
62
) {
63
url = new URL(clientToProxyRequest.url);
64
} else {
65
let providedHost = (clientToProxyRequest.headers.host ??
66
clientToProxyRequest.headers[':authority'] ??
67
'') as string;
68
if (providedHost.endsWith('/')) providedHost = providedHost.slice(0, -1);
69
if (
70
providedHost.startsWith('http://') ||
71
providedHost.startsWith('https://') ||
72
providedHost.startsWith('ws://') ||
73
providedHost.startsWith('wss://')
74
) {
75
providedHost = providedHost.split('://').slice(1).join('://');
76
}
77
// build urls in two steps because URL constructor will bomb on valid WHATWG urls with path
78
url = new URL(`${expectedProtocol}//${providedHost}${clientToProxyRequest.url}`);
79
}
80
81
if (url.protocol !== expectedProtocol) {
82
url.protocol = expectedProtocol;
83
}
84
const state = new Map<ResourceState, Date>();
85
const requestHeaders = parseRawHeaders(clientToProxyRequest.rawHeaders);
86
const ctx: IMitmRequestContext = {
87
id: (this.contextIdCounter += 1),
88
isSSL,
89
isUpgrade,
90
isServerHttp2: false,
91
isHttp2Push: false,
92
method: clientToProxyRequest.method,
93
url,
94
requestSession,
95
requestHeaders,
96
requestOriginalHeaders: parseRawHeaders(clientToProxyRequest.rawHeaders),
97
clientToProxyRequest,
98
proxyToClientResponse,
99
requestTime: new Date(),
100
protocol: (clientToProxyRequest.socket as TLSSocket)?.alpnProtocol || 'http/1.1',
101
documentUrl: clientToProxyRequest.headers.origin as string,
102
originType: this.getOriginType(url, requestHeaders),
103
didBlockResource: false,
104
cacheHandler: null,
105
eventSubscriber: new EventSubscriber(),
106
stateChanges: state,
107
setState(stateStep: ResourceState) {
108
state.set(stateStep, new Date());
109
requestSession.emit('resource-state', { context: ctx, state: stateStep });
110
},
111
};
112
113
if (protocol === 'ws') {
114
ctx.resourceType = 'Websocket';
115
}
116
117
ctx.cacheHandler = new CacheHandler(responseCache, ctx);
118
return ctx;
119
}
120
121
public static createFromHttp2Push(
122
parentContext: IMitmRequestContext,
123
rawHeaders: string[],
124
): IMitmRequestContext {
125
const requestHeaders = parseRawHeaders(rawHeaders);
126
const url = new URL(
127
`${parentContext.url.protocol}//${requestHeaders[':authority']}${requestHeaders[':path']}`,
128
);
129
const state = new Map<ResourceState, Date>();
130
const requestSession = parentContext.requestSession;
131
const ctx = {
132
id: (this.contextIdCounter += 1),
133
url,
134
method: requestHeaders[':method'],
135
isServerHttp2: parentContext.isServerHttp2,
136
requestSession,
137
protocol: parentContext.protocol,
138
remoteAddress: parentContext.remoteAddress,
139
localAddress: parentContext.localAddress,
140
originType: parentContext.originType,
141
isUpgrade: false,
142
isSSL: parentContext.isSSL,
143
hasUserGesture: parentContext.hasUserGesture,
144
isHttp2Push: true,
145
requestOriginalHeaders: parseRawHeaders(rawHeaders),
146
requestHeaders,
147
responseHeaders: null,
148
responseUrl: null,
149
responseTrailers: null,
150
clientToProxyRequest: null,
151
proxyToClientResponse: null,
152
serverToProxyResponseStream: null,
153
proxyToServerRequest: null,
154
requestTime: new Date(),
155
didBlockResource: false,
156
cacheHandler: null,
157
eventSubscriber: new EventSubscriber(),
158
stateChanges: state,
159
setState(stateStep: ResourceState) {
160
state.set(stateStep, new Date());
161
requestSession.emit('resource-state', { context: ctx, state: stateStep });
162
},
163
} as IMitmRequestContext;
164
165
ctx.cacheHandler = new CacheHandler(parentContext.cacheHandler.responseCache, ctx);
166
return ctx;
167
}
168
169
public static toEmittedResource(ctx: IMitmRequestContext): IRequestSessionResponseEvent {
170
const request = {
171
url: ctx.url?.href,
172
headers: ctx.requestHeaders,
173
method: ctx.method,
174
postData: ctx.requestPostData,
175
timestamp: ctx.requestTime.getTime(),
176
} as IResourceRequest;
177
178
const response = {
179
url: ctx.responseUrl,
180
statusCode: ctx.originalStatus ?? ctx.status,
181
statusMessage: ctx.statusMessage,
182
headers: ctx.responseHeaders,
183
trailers: ctx.responseTrailers,
184
timestamp: ctx.responseTime?.getTime(),
185
browserServedFromCache: ctx.browserServedFromCache,
186
browserLoadFailure: ctx.browserLoadFailure,
187
remoteAddress: ctx.remoteAddress,
188
} as IResourceResponse;
189
190
return {
191
id: ctx.id,
192
browserRequestId: ctx.browserRequestId,
193
request,
194
response,
195
documentUrl: ctx.documentUrl,
196
redirectedToUrl: ctx.redirectedToUrl,
197
wasCached: ctx.cacheHandler?.didProposeCachedResource ?? false,
198
resourceType: ctx.resourceType,
199
body: ctx.cacheHandler?.buffer,
200
localAddress: ctx.localAddress,
201
dnsResolvedIp: ctx.dnsResolvedIp,
202
originalHeaders: ctx.requestOriginalHeaders,
203
responseOriginalHeaders: ctx.responseOriginalHeaders,
204
socketId: ctx.proxyToServerMitmSocket?.id,
205
protocol: ctx.protocol,
206
serverAlpn: ctx.proxyToServerMitmSocket?.alpn,
207
didBlockResource: ctx.didBlockResource,
208
executionMillis: (ctx.responseTime ?? new Date()).getTime() - ctx.requestTime.getTime(),
209
isHttp2Push: ctx.isHttp2Push,
210
browserBlockedReason: ctx.browserBlockedReason,
211
browserCanceled: ctx.browserCanceled,
212
};
213
}
214
215
public static assignMitmSocket(ctx: IMitmRequestContext, mitmSocket: MitmSocket): void {
216
ctx.proxyToServerMitmSocket = mitmSocket;
217
ctx.dnsResolvedIp = mitmSocket.dnsResolvedIp;
218
ctx.isServerHttp2 = mitmSocket.isHttp2();
219
ctx.localAddress = mitmSocket.localAddress;
220
ctx.remoteAddress = mitmSocket.remoteAddress;
221
}
222
223
public static getOriginType(url: URL, headers: IResourceHeaders): OriginType {
224
if (isOriginType(headers['Sec-Fetch-Site'] as string)) {
225
return headers['Sec-Fetch-Site'] as OriginType;
226
}
227
228
let origin = (headers.Origin ?? headers.origin) as string;
229
if (!origin) {
230
const referer = (headers.Referer ?? headers.referer) as string;
231
if (referer) origin = new URL(referer).origin;
232
}
233
let originType: OriginType = 'none';
234
if (origin) {
235
const urlOrigin = url.origin;
236
if (urlOrigin === origin) {
237
originType = 'same-origin';
238
} else if (urlOrigin.includes(origin) || origin.includes(urlOrigin)) {
239
originType = 'same-site';
240
} else {
241
originType = 'cross-site';
242
}
243
}
244
return originType;
245
}
246
247
public static readHttp1Response(ctx: IMitmRequestContext, response: http.IncomingMessage): void {
248
ctx.status = response.statusCode;
249
ctx.originalStatus = response.statusCode;
250
ctx.statusMessage = response.statusMessage;
251
252
ctx.responseUrl = response.url;
253
ctx.responseTime = new Date();
254
ctx.serverToProxyResponse = response;
255
ctx.responseOriginalHeaders = parseRawHeaders(response.rawHeaders);
256
ctx.responseHeaders = HeadersHandler.cleanResponseHeaders(ctx, ctx.responseOriginalHeaders);
257
258
const redirectUrl = HeadersHandler.checkForRedirectResponseLocation(ctx);
259
if (redirectUrl) {
260
ctx.redirectedToUrl = redirectUrl.href;
261
ctx.responseUrl = ctx.redirectedToUrl;
262
}
263
}
264
265
public static readHttp2Response(
266
ctx: IMitmRequestContext,
267
response: http2.ClientHttp2Stream,
268
statusCode: number,
269
rawHeaders: string[],
270
): void {
271
const headers = parseRawHeaders(rawHeaders);
272
ctx.status = statusCode;
273
ctx.originalStatus = statusCode;
274
ctx.responseTime = new Date();
275
ctx.serverToProxyResponse = response;
276
ctx.responseOriginalHeaders = headers;
277
ctx.responseHeaders = HeadersHandler.cleanResponseHeaders(ctx, headers);
278
279
const redirectUrl = HeadersHandler.checkForRedirectResponseLocation(ctx);
280
if (redirectUrl) {
281
ctx.redirectedToUrl = redirectUrl.href;
282
ctx.responseUrl = ctx.redirectedToUrl;
283
}
284
}
285
}
286
287