Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/test/MitmRequestAgent.test.ts
1030 views
1
import { IncomingHttpHeaders, IncomingMessage } from 'http';
2
import { URL } from 'url';
3
import * as https from 'https';
4
import * as net from 'net';
5
import * as WebSocket from 'ws';
6
import * as HttpProxyAgent from 'http-proxy-agent';
7
import { Helpers } from '@secret-agent/testing';
8
import { getProxyAgent, runHttpsServer } from '@secret-agent/testing/helpers';
9
import BrowserEmulator from '@secret-agent/default-browser-emulator';
10
import CorePlugins from '@secret-agent/core/lib/CorePlugins';
11
import { IBoundLog } from '@secret-agent/interfaces/ILog';
12
import Log from '@secret-agent/commons/Logger';
13
import MitmServer from '../lib/MitmProxy';
14
import RequestSession from '../handlers/RequestSession';
15
import HeadersHandler from '../handlers/HeadersHandler';
16
import MitmRequestAgent from '../lib/MitmRequestAgent';
17
18
const { log } = Log(module);
19
const browserEmulatorId = BrowserEmulator.id;
20
const selectBrowserMeta = BrowserEmulator.selectBrowserMeta();
21
22
const mocks = {
23
HeadersHandler: {
24
determineResourceType: jest.spyOn(HeadersHandler, 'determineResourceType'),
25
},
26
};
27
28
beforeAll(() => {
29
mocks.HeadersHandler.determineResourceType.mockImplementation(async () => {
30
return {
31
resourceType: 'Document',
32
} as any;
33
});
34
});
35
36
beforeEach(() => {
37
process.env.MITM_ALLOW_INSECURE = 'false';
38
});
39
40
afterAll(Helpers.afterAll);
41
afterEach(Helpers.afterEach);
42
43
test('should create up to a max number of secure connections per origin', async () => {
44
const remotePorts: number[] = [];
45
MitmRequestAgent.defaultMaxConnectionsPerOrigin = 2;
46
const server = await runHttpsServer((req, res) => {
47
remotePorts.push(req.connection.remotePort);
48
res.socket.setKeepAlive(true);
49
res.end('I am here');
50
});
51
const mitmServer = await startMitmServer();
52
53
const session = createMitmSession(mitmServer);
54
55
// @ts-ignore
56
const connectionsByOrigin = session.requestAgent.socketPoolByOrigin;
57
58
const proxyCredentials = session.getProxyCredentials();
59
process.env.MITM_ALLOW_INSECURE = 'true';
60
const promises = [];
61
for (let i = 0; i < 10; i += 1) {
62
// eslint-disable-next-line jest/valid-expect-in-promise
63
const p = Helpers.httpGet(
64
server.baseUrl,
65
`http://localhost:${mitmServer.port}`,
66
proxyCredentials,
67
{ connection: 'keep-alive' },
68
).then(
69
// eslint-disable-next-line promise/always-return
70
res => {
71
expect(res).toBe('I am here');
72
},
73
);
74
promises.push(p);
75
}
76
await Promise.all(promises);
77
78
const host = server.baseUrl.replace('https://', '');
79
// @ts-ignore
80
expect(connectionsByOrigin.get(host).pooled).toBe(2);
81
await session.close();
82
const uniquePorts = new Set<number>(remotePorts);
83
expect(uniquePorts.size).toBe(2);
84
});
85
86
test('should create new connections as needed when no keepalive', async () => {
87
const remotePorts: number[] = [];
88
MitmRequestAgent.defaultMaxConnectionsPerOrigin = 1;
89
const server = await runHttpsServer((req, res) => {
90
remotePorts.push(req.connection.remotePort);
91
res.end('here 2');
92
});
93
const mitmServer = await startMitmServer();
94
95
const session = createMitmSession(mitmServer);
96
97
// @ts-ignore
98
const connectionsByOrigin = session.requestAgent.socketPoolByOrigin;
99
100
const proxyCredentials = session.getProxyCredentials();
101
process.env.MITM_ALLOW_INSECURE = 'true';
102
const promises = [];
103
for (let i = 0; i < 4; i += 1) {
104
// eslint-disable-next-line jest/valid-expect-in-promise
105
const p = Helpers.httpGet(
106
server.baseUrl,
107
`http://localhost:${mitmServer.port}`,
108
proxyCredentials,
109
).then(
110
// eslint-disable-next-line promise/always-return
111
res => {
112
expect(res).toBe('here 2');
113
},
114
);
115
116
promises.push(p);
117
}
118
await Promise.all(promises);
119
120
const host = server.baseUrl.replace('https://', '');
121
// they all close after use, so should be gone now
122
// @ts-ignore
123
expect(connectionsByOrigin.get(host).pooled).toBe(0);
124
125
await session.close();
126
const uniquePorts = new Set<number>(remotePorts);
127
expect(uniquePorts.size).toBe(4);
128
});
129
130
test('should be able to handle a reused socket that closes on server', async () => {
131
MitmRequestAgent.defaultMaxConnectionsPerOrigin = 1;
132
let serverSocket: net.Socket;
133
const sockets = new Set<net.Socket>();
134
const server = await Helpers.runHttpsServer(async (req, res) => {
135
res.writeHead(200, { Connection: 'keep-alive' });
136
res.end('Looks good');
137
serverSocket = res.socket;
138
sockets.add(res.socket);
139
});
140
const mitmServer = await startMitmServer();
141
142
const session = createMitmSession(mitmServer);
143
const proxyCredentials = session.getProxyCredentials();
144
process.env.MITM_ALLOW_INSECURE = 'true';
145
146
{
147
let headers: IncomingHttpHeaders;
148
const response = await Helpers.httpRequest(
149
server.baseUrl,
150
'GET',
151
`http://localhost:${mitmServer.port}`,
152
proxyCredentials,
153
{
154
connection: 'keep-alive',
155
},
156
res => {
157
headers = res.headers;
158
},
159
);
160
expect(headers.connection).toBe('keep-alive');
161
expect(response).toBe('Looks good');
162
}
163
164
// @ts-ignore
165
const originalFn = session.requestAgent.http1Request.bind(session.requestAgent);
166
167
const httpRequestSpy = jest.spyOn<any, any>(session.requestAgent, 'http1Request');
168
httpRequestSpy.mockImplementationOnce(async (ctx, settings) => {
169
serverSocket.destroy();
170
await new Promise(setImmediate);
171
return await originalFn(ctx, settings);
172
});
173
174
{
175
const request = https.request({
176
host: 'localhost',
177
port: server.port,
178
method: 'GET',
179
path: '/',
180
headers: {
181
connection: 'keep-alive',
182
},
183
rejectUnauthorized: false,
184
agent: getProxyAgent(
185
new URL(server.baseUrl),
186
`http://localhost:${mitmServer.port}`,
187
proxyCredentials,
188
),
189
});
190
const responseP = new Promise<IncomingMessage>(resolve => request.on('response', resolve));
191
request.end();
192
const response = await responseP;
193
expect(response.headers.connection).toBe('keep-alive');
194
const body = [];
195
for await (const chunk of response) {
196
body.push(chunk.toString());
197
}
198
expect(body.join('')).toBe('Looks good');
199
}
200
201
expect(sockets.size).toBe(2);
202
expect(httpRequestSpy).toHaveBeenCalledTimes(2);
203
httpRequestSpy.mockClear();
204
});
205
206
test('it should not put upgrade connections in a pool', async () => {
207
const httpServer = await Helpers.runHttpServer();
208
const mitmServer = await startMitmServer();
209
const wsServer = new WebSocket.Server({ noServer: true });
210
211
const session = createMitmSession(mitmServer);
212
213
httpServer.server.on('upgrade', (request, socket, head) => {
214
wsServer.handleUpgrade(request, socket, head, async (ws: WebSocket) => {
215
expect(ws).toBeTruthy();
216
});
217
});
218
219
const wsClient = new WebSocket(`ws://localhost:${httpServer.port}`, {
220
agent: HttpProxyAgent({
221
host: 'localhost',
222
port: mitmServer.port,
223
auth: session.getProxyCredentials(),
224
}),
225
});
226
Helpers.onClose(async () => wsClient.close());
227
228
await new Promise(resolve => wsClient.on('open', resolve));
229
230
// @ts-ignore
231
const pool = session.requestAgent.socketPoolByOrigin.get(`localhost:${httpServer.port}`);
232
// @ts-ignore
233
expect(pool.pooled).toBe(0);
234
});
235
236
test('it should reuse http2 connections', async () => {
237
MitmRequestAgent.defaultMaxConnectionsPerOrigin = 4;
238
const httpServer = await Helpers.runHttp2Server((request, response) => {
239
response.end(request.url);
240
});
241
const baseUrl = httpServer.baseUrl;
242
243
const mitmServer = await startMitmServer();
244
const session = createMitmSession(mitmServer);
245
246
// @ts-ignore
247
const pool = session.requestAgent.socketPoolByOrigin;
248
249
const proxyCredentials = session.getProxyCredentials();
250
251
const proxyUrl = `http://${proxyCredentials}@localhost:${mitmServer.port}`;
252
process.env.MITM_ALLOW_INSECURE = 'true';
253
const results = await Promise.all([
254
Helpers.http2Get(baseUrl, { ':path': '/test1' }, session.sessionId, proxyUrl),
255
Helpers.http2Get(baseUrl, { ':path': '/test2' }, session.sessionId, proxyUrl),
256
Helpers.http2Get(baseUrl, { ':path': '/test3' }, session.sessionId, proxyUrl),
257
]);
258
expect(results).toStrictEqual(['/test1', '/test2', '/test3']);
259
260
process.env.MITM_ALLOW_INSECURE = 'false';
261
const host = baseUrl.replace('https://', '');
262
// not reusable, so should not be here
263
// @ts-ignore
264
expect(pool.get(host).pooled).toBe(0);
265
// @ts-ignore
266
expect(pool.get(host).http2Sessions).toHaveLength(1);
267
});
268
269
async function startMitmServer() {
270
const mitmServer = await MitmServer.start();
271
Helpers.onClose(() => mitmServer.close());
272
return mitmServer;
273
}
274
275
let counter = 1;
276
function createMitmSession(mitmServer: MitmServer) {
277
counter += 1;
278
const plugins = new CorePlugins({ browserEmulatorId, selectBrowserMeta }, log as IBoundLog);
279
const session = new RequestSession(`${counter}`, plugins, null);
280
mitmServer.registerSession(session, false);
281
return session;
282
}
283
284