Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/test/http2.test.ts
1030 views
1
import { Helpers } from '@secret-agent/testing';
2
import * as http2 from 'http2';
3
import { URL } from 'url';
4
import MitmSocket from '@secret-agent/mitm-socket';
5
import IResourceHeaders from '@secret-agent/interfaces/IResourceHeaders';
6
import MitmSocketSession from '@secret-agent/mitm-socket/lib/MitmSocketSession';
7
import BrowserEmulator from '@secret-agent/default-browser-emulator';
8
import CorePlugins from '@secret-agent/core/lib/CorePlugins';
9
import { IBoundLog } from '@secret-agent/interfaces/ILog';
10
import Log from '@secret-agent/commons/Logger';
11
import MitmServer from '../lib/MitmProxy';
12
import RequestSession from '../handlers/RequestSession';
13
import HttpRequestHandler from '../handlers/HttpRequestHandler';
14
import HeadersHandler from '../handlers/HeadersHandler';
15
import MitmRequestContext from '../lib/MitmRequestContext';
16
import { parseRawHeaders } from '../lib/Utils';
17
import CacheHandler from '../handlers/CacheHandler';
18
19
const { log } = Log(module);
20
const browserEmulatorId = BrowserEmulator.id;
21
const selectBrowserMeta = BrowserEmulator.selectBrowserMeta();
22
23
const mocks = {
24
httpRequestHandler: {
25
onRequest: jest.spyOn<any, any>(HttpRequestHandler.prototype, 'onRequest'),
26
},
27
MitmRequestContext: {
28
create: jest.spyOn(MitmRequestContext, 'create'),
29
},
30
HeadersHandler: {
31
determineResourceType: jest.spyOn(HeadersHandler, 'determineResourceType'),
32
},
33
};
34
35
beforeAll(() => {
36
mocks.HeadersHandler.determineResourceType.mockImplementation(async () => {
37
return {
38
resourceType: 'Document',
39
} as any;
40
});
41
});
42
43
process.env.MITM_ALLOW_INSECURE = 'true';
44
beforeEach(() => {
45
mocks.httpRequestHandler.onRequest.mockClear();
46
mocks.MitmRequestContext.create.mockClear();
47
});
48
afterAll(Helpers.afterAll);
49
afterEach(Helpers.afterEach);
50
51
test('should be able to handle an http2->http2 request', async () => {
52
let headers: any;
53
const server = await Helpers.runHttp2Server((req, res1) => {
54
expect(
55
req.rawHeaders
56
.map((x, i) => {
57
if (i % 2 === 0) return x;
58
return undefined;
59
})
60
.filter(Boolean),
61
).toEqual(Object.keys(headers));
62
return res1.end('h2 secure as anything!');
63
});
64
65
headers = {
66
':method': 'GET',
67
':authority': `${server.baseUrl.replace('https://', '')}`,
68
':scheme': 'https',
69
':path': '/temp1',
70
'sec-ch-ua': '"Chromium";v="88", "Google Chrome";v="88", ";Not A Brand";v="99"',
71
'sec-ch-ua-mobile': '?0',
72
'upgrade-insecure-requests': 1,
73
'user-agent':
74
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36',
75
accept:
76
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
77
'sec-fetch-site': 'none',
78
'sec-fetch-mode': 'navigate',
79
'sec-fetch-user': '?1',
80
'sec-fetch-dest': 'document',
81
'accept-encoding': 'gzip, deflate, br',
82
'accept-language': 'en-US,en;q=0.9',
83
};
84
85
const client = await createH2Connection('h2-to-h2', server.baseUrl);
86
87
const h2stream = client.request(headers, { weight: 216 });
88
const buffer = await Helpers.readableToBuffer(h2stream);
89
expect(buffer.toString()).toBe('h2 secure as anything!');
90
91
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(1);
92
const call = mocks.MitmRequestContext.create.mock.calls[0];
93
expect(call[0].isUpgrade).toBe(false);
94
expect(call[0].clientToProxyRequest).toBeInstanceOf(http2.Http2ServerRequest);
95
});
96
97
test('should handle server closing connection', async () => {
98
const server = await Helpers.runHttp2Server((req, res1) => {
99
res1.end('h2 closing soon!');
100
res1.stream.close(2);
101
});
102
103
const client = await createH2Connection('h2-close', server.baseUrl);
104
105
const h2stream = client.request({
106
':path': '/',
107
});
108
const buffer = await Helpers.readableToBuffer(h2stream);
109
expect(buffer.toString()).toBe('h2 closing soon!');
110
});
111
112
test('should send response header arrays through proxy', async () => {
113
const server = await Helpers.runHttp2Server((req, res1) => {
114
res1.setHeader('x-test', ['1', '2']);
115
res1.end('headers done');
116
});
117
118
const client = await createH2Connection('h1-to-h2-response', server.baseUrl);
119
120
const h2stream = client.request({
121
':path': '/',
122
});
123
const h2Headers = new Promise<string[]>(resolve => {
124
h2stream.on('response', (headers, flags, rawHeaders) => {
125
resolve(rawHeaders);
126
});
127
});
128
const buffer = await Helpers.readableToBuffer(h2stream);
129
expect(buffer.toString()).toBe('headers done');
130
131
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(1);
132
const headers = parseRawHeaders(await h2Headers);
133
expect(headers['x-test']).toHaveLength(2);
134
});
135
136
test('should support push streams', async () => {
137
const server = await Helpers.runHttp2Server((req, res1) => {
138
res1.createPushResponse(
139
{
140
':path': '/push1',
141
},
142
(err, pushRes) => {
143
pushRes.end('Push1');
144
},
145
);
146
res1.createPushResponse(
147
{
148
':path': '/push2',
149
'send-1': ['a', 'b'],
150
},
151
(err, pushRes) => {
152
pushRes.setHeader('x-push-test', ['1', '2', '3']);
153
pushRes.end('Push2');
154
},
155
);
156
res1.end('H2 response');
157
});
158
159
const client = await createH2Connection('push-streams', server.baseUrl);
160
const pushRequestHeaders: {
161
[path: string]: { requestHeaders: IResourceHeaders; responseHeaders?: IResourceHeaders };
162
} = {};
163
client.on('stream', (stream, headers1, flags, rawHeaders) => {
164
const path = headers1[':path'];
165
pushRequestHeaders[path] = { requestHeaders: parseRawHeaders(rawHeaders) };
166
stream.on('push', (responseHeaders, responseFalgs, rawResponseHeaders) => {
167
pushRequestHeaders[path].responseHeaders = parseRawHeaders(rawResponseHeaders);
168
});
169
});
170
const h2stream = client.request({ ':path': '/' });
171
const buffer = await Helpers.readableToBuffer(h2stream);
172
expect(buffer.toString()).toBe('H2 response');
173
expect(pushRequestHeaders['/push1']).toBeTruthy();
174
expect(pushRequestHeaders['/push2']).toBeTruthy();
175
expect(pushRequestHeaders['/push2'].responseHeaders['x-push-test']).toStrictEqual([
176
'1',
177
'2',
178
'3',
179
]);
180
expect(pushRequestHeaders['/push2'].requestHeaders['send-1']).toStrictEqual(['a', 'b']);
181
});
182
183
test('should handle cache headers for h2', async () => {
184
const etags: string[] = [];
185
CacheHandler.isEnabled = true;
186
Helpers.onClose(() => (CacheHandler.isEnabled = false));
187
const server = await Helpers.runHttp2Server((req, res1) => {
188
if (req.headers[':path'] === '/cached') {
189
etags.push(req.headers['if-none-match'] as string);
190
res1.setHeader('etag', '"46e2aa1bef425becb0cb4651c23fff38:1573670083.753497"');
191
return res1.end(Buffer.from(['a', 'c']));
192
}
193
return res1.end('bad data');
194
});
195
196
const client = await createH2Connection('cached-etag', server.baseUrl);
197
const res1 = await client.request({ ':path': '/cached' });
198
expect(res1).toBeTruthy();
199
await new Promise(resolve => res1.once('response', resolve));
200
expect(etags[0]).not.toBeTruthy();
201
202
const res2 = await client.request({ ':path': '/cached' });
203
expect(res2).toBeTruthy();
204
await new Promise(resolve => res2.once('response', resolve));
205
expect(etags[1]).toBe('"46e2aa1bef425becb0cb4651c23fff38:1573670083.753497"');
206
207
const res3 = await client.request({ ':path': '/cached', 'if-none-match': 'etag2' });
208
expect(res3).toBeTruthy();
209
await new Promise(resolve => res3.once('response', resolve));
210
expect(etags[2]).toBe('etag2');
211
});
212
213
test('should send trailers', async () => {
214
const server = await Helpers.runHttp2Server((req, res1) => {
215
res1.writeHead(200, { header1: 'test' });
216
res1.addTrailers({
217
'mr-trailer': '1',
218
});
219
return res1.end('Trailin...');
220
});
221
222
const client = await createH2Connection('trailers', server.baseUrl);
223
224
const h2stream = client.request({ ':path': '/' });
225
const trailers = await new Promise(resolve => h2stream.once('trailers', resolve));
226
const buffer = await Helpers.readableToBuffer(h2stream);
227
expect(buffer.toString()).toBe('Trailin...');
228
expect(trailers['mr-trailer']).toBe('1');
229
});
230
231
async function createH2Connection(sessionIdPrefix: string, url: string) {
232
const hostUrl = new URL(url);
233
const mitmServer = await MitmServer.start();
234
Helpers.onClose(() => mitmServer.close());
235
236
const session = createSession(mitmServer, sessionIdPrefix);
237
const sessionId = session.sessionId;
238
const proxyCredentials = session.getProxyCredentials();
239
const proxyHost = `http://${proxyCredentials}@localhost:${mitmServer.port}`;
240
const mitmSocketSession = new MitmSocketSession(sessionId, {
241
clientHelloId: 'chrome-72',
242
rejectUnauthorized: false,
243
});
244
Helpers.needsClosing.push(mitmSocketSession);
245
246
const tlsConnection = new MitmSocket(sessionId, {
247
host: 'localhost',
248
port: hostUrl.port,
249
servername: 'localhost',
250
keepAlive: true,
251
isSsl: url.startsWith('https'),
252
proxyUrl: proxyHost,
253
});
254
Helpers.onClose(async () => tlsConnection.close());
255
await tlsConnection.connect(mitmSocketSession);
256
const client = http2.connect(url, {
257
createConnection: () => tlsConnection.socket,
258
});
259
Helpers.onClose(async () => client.close());
260
return client;
261
}
262
263
let sessionCounter = 0;
264
function createSession(mitmProxy: MitmServer, sessionId = '') {
265
const plugins = new CorePlugins({ browserEmulatorId, selectBrowserMeta }, log as IBoundLog);
266
const session = new RequestSession(
267
`${sessionId}${(sessionCounter += 1)}`,
268
plugins,
269
);
270
mitmProxy.registerSession(session, false);
271
Helpers.needsClosing.push(session);
272
273
return session;
274
}
275
276