Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/handlers/HeadersHandler.ts
1030 views
1
import IResourceHeaders from '@secret-agent/interfaces/IResourceHeaders';
2
import * as http from 'http';
3
import * as http2 from 'http2';
4
import OriginType from '@secret-agent/interfaces/OriginType';
5
import ResourceType from '@secret-agent/interfaces/ResourceType';
6
import { URL } from 'url';
7
import IHttpResourceLoadDetails from '@secret-agent/interfaces/IHttpResourceLoadDetails';
8
import { parseRawHeaders } from '../lib/Utils';
9
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
10
import ResourceState from '../interfaces/ResourceState';
11
12
const redirectCodes = new Set([300, 301, 302, 303, 305, 307, 308]);
13
14
const {
15
HTTP2_HEADER_PATH,
16
HTTP2_HEADER_STATUS,
17
HTTP2_HEADER_AUTHORITY,
18
HTTP2_HEADER_SCHEME,
19
HTTP2_HEADER_METHOD,
20
} = http2.constants;
21
22
const SecFetchDest = 'sec-fetch-dest';
23
const SecFetchSite = 'sec-fetch-site';
24
const SecFetchUser = 'sec-fetch-user';
25
const SecFetchMode = 'sec-fetch-mode';
26
const PublicKeyPins = 'public-key-pins';
27
const Http2Settings = 'http2-settings';
28
29
const nodeVersion = process.version.replace('v', '').split('.').map(Number);
30
export default class HeadersHandler {
31
public static async determineResourceType(ctx: IMitmRequestContext): Promise<void> {
32
ctx.setState(ResourceState.DetermineResourceType);
33
const session = ctx.requestSession;
34
35
const { method, requestHeaders } = ctx;
36
37
const fetchDest = this.getRequestHeader<string>(ctx, SecFetchDest);
38
const fetchSite = this.getRequestHeader<string>(ctx, SecFetchSite);
39
const fetchMode = this.getRequestHeader<string>(ctx, SecFetchMode);
40
const hasUserActivity = this.getRequestHeader<string>(ctx, SecFetchUser);
41
const isDocumentNavigation = fetchMode === 'navigate' && fetchDest === 'document';
42
43
// fill in known details
44
if (fetchSite) ctx.originType = fetchSite as OriginType;
45
46
if (fetchDest) ctx.resourceType = resourceTypesBySecFetchDest.get(fetchDest as string);
47
if (method === 'OPTIONS') ctx.resourceType = 'Preflight';
48
49
if (hasUserActivity === '?1') ctx.hasUserGesture = true;
50
if (fetchMode) ctx.isUserNavigation = isDocumentNavigation && ctx.hasUserGesture;
51
52
const requestedResource = session.browserRequestMatcher.onMitmRequestedResource(ctx);
53
54
// if we're going to block this, don't wait for a
55
if (!ctx.resourceType && session.shouldBlockRequest(ctx.url.href)) {
56
requestedResource.browserRequestedPromise.resolve();
57
}
58
59
if (ctx.resourceType === 'Websocket') {
60
ctx.browserRequestId = await session.getWebsocketUpgradeRequestId(requestHeaders);
61
requestedResource.browserRequestedPromise.resolve();
62
} else if (!ctx.resourceType || ctx.resourceType === 'Fetch') {
63
// if fetch, we need to wait for the browser request so we can see if we should use xhr order or fetch order
64
await ctx.browserHasRequested;
65
}
66
}
67
68
public static getRequestHeader<T = string | string[]>(
69
ctx: Pick<IHttpResourceLoadDetails, 'requestHeaders'>,
70
name: string,
71
): T {
72
const lowerName = toLowerCase(name);
73
const exactMatch = ctx.requestHeaders[name] ?? ctx.requestHeaders[lowerName];
74
if (exactMatch) return exactMatch as any;
75
for (const [key, value] of Object.entries(ctx.requestHeaders)) {
76
if (toLowerCase(key) === lowerName) {
77
return value as any;
78
}
79
}
80
}
81
82
public static cleanResponseHeaders(
83
ctx: IMitmRequestContext,
84
originalRawHeaders: IResourceHeaders,
85
): IResourceHeaders {
86
const headers: IResourceHeaders = {};
87
for (const [headerName, value] of Object.entries(originalRawHeaders)) {
88
const canonizedKey = headerName.trim();
89
90
const lowerHeaderName = toLowerCase(canonizedKey);
91
if (
92
// HPKP header => filter
93
lowerHeaderName === PublicKeyPins ||
94
// H2 status not allowed twice - we re-add
95
lowerHeaderName === HTTP2_HEADER_STATUS ||
96
lowerHeaderName === Http2Settings
97
) {
98
continue;
99
}
100
101
if (Array.isArray(value)) {
102
if (singleValueHttp2Headers.has(lowerHeaderName)) {
103
headers[canonizedKey] = value[0];
104
} else {
105
headers[canonizedKey] = [...value].filter(x => !checkInvalidHeaderChar(x));
106
}
107
} else {
108
if (checkInvalidHeaderChar(value)) continue;
109
headers[canonizedKey] = value;
110
}
111
}
112
113
return headers;
114
}
115
116
public static checkForRedirectResponseLocation(context: IMitmRequestContext): URL {
117
if (redirectCodes.has(context.status)) {
118
const redirectLocation = context.responseHeaders.location || context.responseHeaders.Location;
119
if (redirectLocation) {
120
return new URL(redirectLocation as string, context.url);
121
}
122
}
123
}
124
125
public static sendRequestTrailers(ctx: IMitmRequestContext): void {
126
const clientRequest = ctx.clientToProxyRequest;
127
if (!clientRequest.trailers) return;
128
129
const trailers = parseRawHeaders(clientRequest.rawTrailers);
130
ctx.requestTrailers = trailers;
131
132
if (ctx.proxyToServerRequest instanceof http.ClientRequest) {
133
ctx.proxyToServerRequest.addTrailers(trailers ?? {});
134
} else {
135
const stream = ctx.proxyToServerRequest;
136
stream.on('wantTrailers', () => {
137
if (!trailers) stream.close();
138
else stream.sendTrailers(trailers ?? {});
139
});
140
}
141
}
142
143
public static prepareHttp2RequestHeadersForSave(
144
headers: IMitmRequestContext['requestHeaders'],
145
): IMitmRequestContext['requestHeaders'] {
146
const order: string[] = [];
147
for (const key of Object.keys(headers)) {
148
if (key.startsWith(':')) order.unshift(key);
149
else order.push(key);
150
}
151
const newHeaders = {};
152
for (const key of order) {
153
newHeaders[key] = headers[key];
154
}
155
return newHeaders;
156
}
157
158
public static prepareRequestHeadersForHttp2(ctx: IMitmRequestContext): void {
159
const url = ctx.url;
160
const oldHeaders = ctx.requestHeaders;
161
ctx.requestHeaders = Object.create(null);
162
163
// WORKAROUND: nodejs inserts headers in reverse to front of list, so will mess with the order
164
// to workaround, insert in reverse order
165
// https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/http2/util.js#L521
166
let headers: IResourceHeaders = {
167
[HTTP2_HEADER_METHOD]: ctx.method,
168
[HTTP2_HEADER_AUTHORITY]:
169
oldHeaders[HTTP2_HEADER_AUTHORITY] ?? this.getRequestHeader<string>(ctx, 'host'),
170
[HTTP2_HEADER_SCHEME]: 'https',
171
[HTTP2_HEADER_PATH]: url.pathname + url.search,
172
};
173
174
if (nodeHasPseudoHeaderPatch()) {
175
headers = {
176
[HTTP2_HEADER_PATH]: url.pathname + url.search,
177
[HTTP2_HEADER_SCHEME]: 'https',
178
[HTTP2_HEADER_AUTHORITY]:
179
oldHeaders[HTTP2_HEADER_AUTHORITY] ?? this.getRequestHeader<string>(ctx, 'host'),
180
[HTTP2_HEADER_METHOD]: ctx.method,
181
};
182
}
183
Object.assign(ctx.requestHeaders, headers);
184
185
for (const header of Object.keys(oldHeaders)) {
186
const lowerKey = toLowerCase(header);
187
if (stripHttp1HeadersForH2.has(lowerKey) || lowerKey.startsWith('proxy-')) {
188
continue;
189
}
190
191
if (!header.startsWith(':')) {
192
ctx.requestHeaders[header] = oldHeaders[header];
193
}
194
if (singleValueHttp2Headers.has(lowerKey)) {
195
const value = ctx.requestHeaders[header];
196
if (Array.isArray(value) && value.length) {
197
ctx.requestHeaders[header] = value[0];
198
}
199
}
200
}
201
}
202
203
public static cleanPushHeaders(ctx: IMitmRequestContext): void {
204
for (const key of Object.keys(ctx.requestHeaders)) {
205
const lowerKey = toLowerCase(key);
206
if (stripHttp1HeadersForH2.has(lowerKey) || lowerKey.startsWith('proxy-')) {
207
delete ctx.requestHeaders[key];
208
}
209
if (singleValueHttp2Headers.has(lowerKey)) {
210
const value = ctx.requestHeaders[key];
211
if (Array.isArray(value) && value.length) {
212
ctx.requestHeaders[key] = value[0];
213
}
214
}
215
}
216
}
217
218
public static cleanProxyHeaders(ctx: IMitmRequestContext): void {
219
const headers = ctx.requestHeaders;
220
for (const header of Object.keys(headers)) {
221
if (toLowerCase(header).startsWith('proxy-')) {
222
delete headers[header];
223
}
224
}
225
}
226
}
227
228
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
229
/**
230
* True if val contains an invalid field-vchar
231
* field-value = *( field-content / obs-fold )
232
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
233
* field-vchar = VCHAR / obs-text
234
*/
235
function checkInvalidHeaderChar(val): boolean {
236
return headerCharRegex.test(val);
237
}
238
239
const lowerCaseMap = new Map<string, string>();
240
241
function toLowerCase(header: string): string {
242
if (!lowerCaseMap.has(header)) lowerCaseMap.set(header, header.toLowerCase());
243
return lowerCaseMap.get(header);
244
}
245
246
const resourceTypesBySecFetchDest = new Map<string, ResourceType>([
247
['document', 'Document'],
248
['nested-document', 'Document'],
249
['iframe', 'Document'],
250
251
['style', 'Stylesheet'],
252
['xslt', 'Stylesheet'], // not sure where this one goes
253
['script', 'Script'],
254
255
['empty', 'Fetch'],
256
['font', 'Font'],
257
['image', 'Image'],
258
['video', 'Media'],
259
['audio', 'Media'],
260
['paintworklet', 'Media'], // guess
261
['audioworklet', 'Media'], // guess
262
['manifest', 'Manifest'],
263
['embed', 'Other'], // guess
264
['object', 'Other'], // guess
265
['report', 'CSP Violation Report'],
266
['worker', 'Other'],
267
['serviceworker', 'Other'],
268
['sharedworker', 'Other'],
269
['track', 'Text Track'], // guess
270
]);
271
272
const stripHttp1HeadersForH2 = new Set([
273
http2.constants.HTTP2_HEADER_CONNECTION,
274
http2.constants.HTTP2_HEADER_UPGRADE,
275
http2.constants.HTTP2_HEADER_HTTP2_SETTINGS,
276
http2.constants.HTTP2_HEADER_KEEP_ALIVE,
277
http2.constants.HTTP2_HEADER_PROXY_CONNECTION,
278
http2.constants.HTTP2_HEADER_TRANSFER_ENCODING,
279
'host',
280
'via',
281
'forwarded',
282
]);
283
// This set contains headers that are permitted to have only a single
284
// value. Multiple instances must not be specified.
285
// NOTE: some are not exposed in constants, so we're putting strings in place
286
const singleValueHttp2Headers = new Set([
287
http2.constants.HTTP2_HEADER_STATUS,
288
http2.constants.HTTP2_HEADER_METHOD,
289
http2.constants.HTTP2_HEADER_AUTHORITY,
290
http2.constants.HTTP2_HEADER_SCHEME,
291
http2.constants.HTTP2_HEADER_PATH,
292
':protocol',
293
'access-control-allow-credentials',
294
'access-control-max-age',
295
'access-control-request-method',
296
http2.constants.HTTP2_HEADER_AGE,
297
http2.constants.HTTP2_HEADER_AUTHORIZATION,
298
http2.constants.HTTP2_HEADER_CONTENT_ENCODING,
299
http2.constants.HTTP2_HEADER_CONTENT_LANGUAGE,
300
http2.constants.HTTP2_HEADER_CONTENT_LENGTH,
301
http2.constants.HTTP2_HEADER_CONTENT_LOCATION,
302
http2.constants.HTTP2_HEADER_CONTENT_MD5,
303
http2.constants.HTTP2_HEADER_CONTENT_RANGE,
304
http2.constants.HTTP2_HEADER_CONTENT_TYPE,
305
http2.constants.HTTP2_HEADER_DATE,
306
'dnt',
307
http2.constants.HTTP2_HEADER_ETAG,
308
http2.constants.HTTP2_HEADER_EXPIRES,
309
http2.constants.HTTP2_HEADER_FROM,
310
http2.constants.HTTP2_HEADER_HOST,
311
http2.constants.HTTP2_HEADER_IF_MATCH,
312
http2.constants.HTTP2_HEADER_IF_MODIFIED_SINCE,
313
http2.constants.HTTP2_HEADER_IF_NONE_MATCH,
314
http2.constants.HTTP2_HEADER_IF_RANGE,
315
http2.constants.HTTP2_HEADER_IF_UNMODIFIED_SINCE,
316
http2.constants.HTTP2_HEADER_LAST_MODIFIED,
317
http2.constants.HTTP2_HEADER_LOCATION,
318
http2.constants.HTTP2_HEADER_MAX_FORWARDS,
319
http2.constants.HTTP2_HEADER_PROXY_AUTHORIZATION,
320
http2.constants.HTTP2_HEADER_RANGE,
321
http2.constants.HTTP2_HEADER_REFERER,
322
http2.constants.HTTP2_HEADER_RETRY_AFTER,
323
'tk',
324
'upgrade-insecure-requests',
325
http2.constants.HTTP2_HEADER_USER_AGENT,
326
'x-content-type-options',
327
]);
328
329
function nodeHasPseudoHeaderPatch(): boolean {
330
const [nodeVersionMajor, nodeVersionMinor, nodeVersionPatch] = nodeVersion;
331
332
// Node.js was reversing pseudo-headers as provided. Fixed in 17.5.0, 16.14.1
333
let needsReverseHeaders = nodeVersionMajor <= 17;
334
if (nodeVersionMajor === 17 && nodeVersionMinor >= 5) needsReverseHeaders = false;
335
if (nodeVersionMajor === 16 && nodeVersionMinor === 14 && nodeVersionPatch >= 1)
336
needsReverseHeaders = false;
337
if (nodeVersionMajor === 16 && nodeVersionMinor > 14) needsReverseHeaders = false;
338
return needsReverseHeaders;
339
}
340
341