Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/test/basic.test.ts
1030 views
1
import * as http from 'http';
2
import { IncomingHttpHeaders } from 'http';
3
import { Helpers } from '@secret-agent/testing';
4
import * as HttpProxyAgent from 'http-proxy-agent';
5
import { URL } from 'url';
6
import { AddressInfo } from 'net';
7
import * as WebSocket from 'ws';
8
import * as Url from 'url';
9
import { createPromise } from '@secret-agent/commons/utils';
10
import IHttpResourceLoadDetails from '@secret-agent/interfaces/IHttpResourceLoadDetails';
11
import BrowserEmulator from '@secret-agent/default-browser-emulator';
12
import CorePlugins from '@secret-agent/core/lib/CorePlugins';
13
import { IBoundLog } from '@secret-agent/interfaces/ILog';
14
import Log from '@secret-agent/commons/Logger';
15
import HttpRequestHandler from '../handlers/HttpRequestHandler';
16
import RequestSession, { IRequestSessionRequestEvent } from '../handlers/RequestSession';
17
import MitmServer from '../lib/MitmProxy';
18
import HeadersHandler from '../handlers/HeadersHandler';
19
import HttpUpgradeHandler from '../handlers/HttpUpgradeHandler';
20
import { parseRawHeaders } from '../lib/Utils';
21
22
const { log } = Log(module);
23
const browserEmulatorId = BrowserEmulator.id;
24
const selectBrowserMeta = BrowserEmulator.selectBrowserMeta();
25
26
const mocks = {
27
httpRequestHandler: {
28
onRequest: jest.spyOn<any, any>(HttpRequestHandler.prototype, 'onRequest'),
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
beforeEach(() => {
44
mocks.httpRequestHandler.onRequest.mockClear();
45
});
46
afterAll(Helpers.afterAll);
47
afterEach(Helpers.afterEach);
48
49
let sessionCounter = 1;
50
51
describe('basic MitM tests', () => {
52
it('should send request through proxy', async () => {
53
const httpServer = await Helpers.runHttpServer();
54
const mitmServer = await MitmServer.start();
55
Helpers.needsClosing.push(mitmServer);
56
const proxyHost = `http://localhost:${mitmServer.port}`;
57
58
const session = createSession(mitmServer);
59
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
60
61
const res = await Helpers.httpGet(httpServer.url, proxyHost, session.getProxyCredentials());
62
expect(res.includes('Hello')).toBeTruthy();
63
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(1);
64
65
await mitmServer.close();
66
});
67
68
it('should return http1 response headers through proxy', async () => {
69
const httpServer = await Helpers.runHttpServer({
70
addToResponse(response) {
71
response.setHeader('x-test', ['1', '2']);
72
},
73
});
74
const mitmServer = await MitmServer.start();
75
Helpers.needsClosing.push(mitmServer);
76
const proxyHost = `http://localhost:${mitmServer.port}`;
77
78
const session = createSession(mitmServer);
79
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
80
81
let rawHeaders: string[] = null;
82
const res = await Helpers.httpRequest(
83
httpServer.url,
84
'GET',
85
proxyHost,
86
session.getProxyCredentials(),
87
{},
88
getRes => {
89
rawHeaders = getRes.rawHeaders;
90
},
91
);
92
const headers = parseRawHeaders(rawHeaders);
93
expect(res.includes('Hello')).toBeTruthy();
94
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(1);
95
expect(headers['x-test']).toHaveLength(2);
96
97
await mitmServer.close();
98
});
99
100
it('should be able to man-in-the-middle an https request', async () => {
101
const server = await Helpers.runHttpsServer((req, res1) => {
102
return res1.end('Secure as anything!');
103
});
104
105
const mitmServer = await MitmServer.start();
106
Helpers.needsClosing.push(mitmServer);
107
const proxyHost = `http://localhost:${mitmServer.port}`;
108
109
const session = createSession(mitmServer);
110
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
111
112
process.env.MITM_ALLOW_INSECURE = 'true';
113
const res = await Helpers.httpGet(server.baseUrl, proxyHost, session.getProxyCredentials());
114
expect(res.includes('Secure as anything!')).toBeTruthy();
115
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(1);
116
process.env.MITM_ALLOW_INSECURE = 'false';
117
});
118
119
it('should send an https request through upstream proxy', async () => {
120
const httpServer = await Helpers.runHttpServer();
121
const mitmServer = await MitmServer.start();
122
Helpers.needsClosing.push(mitmServer);
123
const proxyHost = `http://localhost:${mitmServer.port}`;
124
const upstreamProxyHost = httpServer.url.replace(/\/$/, '');
125
126
let upstreamProxyConnected = false;
127
httpServer.on('connect', (req: http.IncomingMessage, socket: any) => {
128
upstreamProxyConnected = true;
129
socket.end();
130
});
131
132
const session = createSession(mitmServer, upstreamProxyHost);
133
134
await Helpers.httpGet(
135
'https://dataliberationfoundation.org',
136
proxyHost,
137
session.getProxyCredentials(),
138
).catch();
139
140
expect(upstreamProxyConnected).toBeTruthy();
141
});
142
143
it('should support http calls through the mitm', async () => {
144
let headers: IncomingHttpHeaders;
145
const server = http
146
.createServer((req, res) => {
147
headers = req.headers;
148
return res.end('Ok');
149
})
150
.listen(0)
151
.unref();
152
Helpers.onClose(
153
() =>
154
new Promise<void>(resolve => {
155
server.close(() => resolve());
156
}),
157
);
158
159
const serverPort = (server.address() as AddressInfo).port;
160
161
const mitmServer = await MitmServer.start();
162
Helpers.needsClosing.push(mitmServer);
163
const proxyHost = `http://localhost:${mitmServer.port}`;
164
165
const session = createSession(mitmServer);
166
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
167
168
const res = await Helpers.httpGet(
169
`http://localhost:${serverPort}`,
170
proxyHost,
171
session.getProxyCredentials(),
172
);
173
expect(res).toBe('Ok');
174
expect(headers['proxy-authorization']).not.toBeTruthy();
175
176
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(1);
177
});
178
179
it('should strip proxy headers', async () => {
180
const httpServer = await Helpers.runHttpServer({
181
onRequest(url, method, headers1) {
182
expect(url).toBe('/page1');
183
expect(method).toBe('GET');
184
expect(Object.keys(headers1).filter(x => x.startsWith('proxy-'))).toHaveLength(0);
185
expect(headers1.last).toBe('1');
186
},
187
});
188
const mitmServer = await MitmServer.start();
189
Helpers.needsClosing.push(mitmServer);
190
const proxyHost = `http://localhost:${mitmServer.port}`;
191
192
const session = createSession(mitmServer);
193
194
await Helpers.httpGet(`${httpServer.url}page1`, proxyHost, session.getProxyCredentials(), {
195
'proxy-authorization': `Basic ${Buffer.from(session.getProxyCredentials()).toString(
196
'base64',
197
)}`,
198
last: '1',
199
}).catch();
200
201
await httpServer.close();
202
await mitmServer.close();
203
});
204
205
it('should copy post data', async () => {
206
const httpServer = await Helpers.runHttpServer();
207
const mitmServer = await MitmServer.start();
208
Helpers.needsClosing.push(mitmServer);
209
const proxyHost = `http://localhost:${mitmServer.port}`;
210
const session = createSession(mitmServer);
211
212
const resourcePromise = new Promise<IRequestSessionRequestEvent>(resolve =>
213
session.on('response', resolve),
214
);
215
await Helpers.httpRequest(
216
`${httpServer.url}page2`,
217
'POST',
218
proxyHost,
219
session.getProxyCredentials(),
220
{
221
'content-type': 'application/json',
222
},
223
null,
224
Buffer.from(JSON.stringify({ gotData: true, isCompressed: 'no' })),
225
);
226
227
expect(session.requestedUrls).toHaveLength(1);
228
229
const resource = await resourcePromise;
230
expect(resource.request.postData).toBeTruthy();
231
expect(resource.request.postData.toString()).toBe(
232
JSON.stringify({ gotData: true, isCompressed: 'no' }),
233
);
234
235
await httpServer.close();
236
await mitmServer.close();
237
});
238
239
it('should support large post data', async () => {
240
const httpServer = await Helpers.runHttpServer();
241
const mitmServer = await MitmServer.start();
242
Helpers.needsClosing.push(mitmServer);
243
const proxyHost = `http://localhost:${mitmServer.port}`;
244
245
const session = createSession(mitmServer);
246
247
const proxyCredentials = session.getProxyCredentials();
248
const buffers: Buffer[] = [];
249
const copyBuffer = Buffer.from('ASDGASDFASDWERWER@#$%#$%#$%#$%#DSFSFGDBSDFGD$%^$%^$%');
250
for (let i = 0; i <= 10e4; i += 1) {
251
buffers.push(copyBuffer);
252
}
253
254
const largeBuffer = Buffer.concat(buffers);
255
256
const resourcePromise = new Promise<IRequestSessionRequestEvent>(resolve =>
257
session.on('response', resolve),
258
);
259
await Helpers.httpRequest(
260
`${httpServer.url}page2`,
261
'POST',
262
proxyHost,
263
proxyCredentials,
264
{
265
'content-type': 'application/json',
266
},
267
null,
268
Buffer.from(JSON.stringify({ largeBuffer: largeBuffer.toString('hex') })),
269
);
270
271
const resource = await resourcePromise;
272
expect(session.requestedUrls).toHaveLength(1);
273
expect(resource.request.postData.toString()).toBe(
274
JSON.stringify({ largeBuffer: largeBuffer.toString('hex') }),
275
);
276
277
await httpServer.close();
278
await mitmServer.close();
279
});
280
281
it('should modify websocket upgrade headers', async () => {
282
const httpServer = await Helpers.runHttpServer();
283
const mitmServer = await MitmServer.start();
284
const upgradeSpy = jest.spyOn(HttpUpgradeHandler.prototype, 'onUpgrade');
285
const requestSpy = jest.spyOn(HttpRequestHandler.prototype, 'onRequest');
286
Helpers.needsClosing.push(mitmServer);
287
288
const serverMessages = [];
289
const serverMessagePromise = createPromise();
290
const wsServer = new WebSocket.Server({ noServer: true });
291
const session = createSession(mitmServer);
292
293
httpServer.server.on('upgrade', (request, socket, head) => {
294
// ensure header is stripped
295
expect(request.headers).toBeTruthy();
296
for (const key of Object.keys(session.getProxyCredentials())) {
297
expect(request.headers).not.toHaveProperty(key);
298
}
299
300
wsServer.handleUpgrade(request, socket, head, async (ws: WebSocket) => {
301
ws.on('message', msg => {
302
expect(msg).toMatch(/Hi\d+/);
303
serverMessages.push(msg);
304
if (serverMessages.length === 20) serverMessagePromise.resolve();
305
});
306
for (let i = 0; i < 20; i += 1) {
307
ws.send(`Message${i}`);
308
await new Promise(setImmediate);
309
}
310
});
311
});
312
313
const wsClient = new WebSocket(`ws://localhost:${httpServer.port}`, {
314
agent: HttpProxyAgent({
315
...Url.parse(`http://localhost:${mitmServer.port}`),
316
auth: session.getProxyCredentials(),
317
}),
318
});
319
320
Helpers.onClose(async () => wsClient.close());
321
322
const messagePromise = createPromise();
323
const msgs = [];
324
wsClient.on('open', async () => {
325
wsClient.on('message', msg => {
326
expect(msg).toMatch(/Message\d+/);
327
msgs.push(msg);
328
if (msgs.length === 20) {
329
messagePromise.resolve();
330
}
331
});
332
for (let i = 0; i < 20; i += 1) {
333
wsClient.send(`Hi${i}`);
334
await new Promise(setImmediate);
335
}
336
});
337
await messagePromise.promise;
338
await serverMessagePromise;
339
expect(upgradeSpy).toHaveBeenCalledTimes(1);
340
expect(requestSpy).not.toHaveBeenCalled();
341
});
342
343
it('should intercept requests', async () => {
344
mocks.HeadersHandler.determineResourceType.mockRestore();
345
const httpServer = await Helpers.runHttpServer();
346
const mitmServer = await MitmServer.start();
347
Helpers.needsClosing.push(mitmServer);
348
const proxyHost = `http://localhost:${mitmServer.port}`;
349
350
const session = createSession(mitmServer);
351
session.plugins.beforeHttpRequest = jest.fn();
352
session.browserRequestMatcher.onBrowserRequestedResource(
353
{
354
browserRequestId: '25.123',
355
url: new URL(`${httpServer.url}page1`),
356
method: 'GET',
357
resourceType: 'Document',
358
hasUserGesture: true,
359
isUserNavigation: true,
360
requestHeaders: {},
361
documentUrl: `${httpServer.url}page1`,
362
} as IHttpResourceLoadDetails,
363
1,
364
);
365
const onresponse = jest.fn();
366
const onError = jest.fn();
367
session.on('response', onresponse);
368
session.on('http-error', onError);
369
370
const proxyCredentials = session.getProxyCredentials();
371
372
await Helpers.httpGet(`${httpServer.url}page1`, proxyHost, proxyCredentials);
373
374
expect(session.plugins.beforeHttpRequest).toHaveBeenCalledTimes(1);
375
expect(onresponse).toHaveBeenCalledTimes(1);
376
377
const [responseEvent] = onresponse.mock.calls[0];
378
const { request, response, wasCached, resourceType, body } = responseEvent;
379
expect(body).toBeInstanceOf(Buffer);
380
expect(body.toString()).toBeTruthy();
381
expect(response).toBeTruthy();
382
expect(request.url).toBe(`${httpServer.url}page1`);
383
expect(resourceType).toBe('Document');
384
expect(response.remoteAddress).toContain(httpServer.port);
385
expect(wasCached).toBe(false);
386
expect(onError).not.toHaveBeenCalled();
387
mocks.HeadersHandler.determineResourceType.mockImplementation(async () => ({} as any));
388
389
await httpServer.close();
390
await mitmServer.close();
391
});
392
});
393
394
function createSession(mitmProxy: MitmServer, upstreamProxyUrl: string = null) {
395
const plugins = new CorePlugins({ browserEmulatorId, selectBrowserMeta }, log as IBoundLog);
396
const session = new RequestSession(`${(sessionCounter += 1)}`, plugins, upstreamProxyUrl);
397
mitmProxy.registerSession(session, false);
398
Helpers.needsClosing.push(session);
399
400
return session;
401
}
402
403