Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/test/mitm.test.ts
1029 views
1
import { Helpers } from '@secret-agent/testing';
2
import MitmRequestContext from '@secret-agent/mitm/lib/MitmRequestContext';
3
import { createPromise } from '@secret-agent/commons/utils';
4
import { LocationStatus } from '@secret-agent/interfaces/Location';
5
import { ITestKoaServer } from '@secret-agent/testing/helpers';
6
import Resolvable from '@secret-agent/commons/Resolvable';
7
import GlobalPool from '../lib/GlobalPool';
8
import Core, { Session } from '../index';
9
10
const mocks = {
11
MitmRequestContext: {
12
create: jest.spyOn(MitmRequestContext, 'create'),
13
},
14
};
15
16
let koa: ITestKoaServer;
17
beforeAll(async () => {
18
koa = await Helpers.runKoaServer(true);
19
await GlobalPool.start();
20
});
21
22
beforeEach(async () => {
23
mocks.MitmRequestContext.create.mockClear();
24
});
25
26
afterAll(Helpers.afterAll);
27
afterEach(Helpers.afterEach);
28
29
test('should send a Host header to secure http1 Chrome requests', async () => {
30
let rawHeaders: string[] = [];
31
32
const server = await Helpers.runHttpsServer((req, res) => {
33
rawHeaders = req.rawHeaders;
34
res.end('<html>Loaded</html>');
35
});
36
37
const url = `${server.baseUrl}/`;
38
const session = await GlobalPool.createSession({});
39
Helpers.needsClosing.push(session);
40
const tab = await session.createTab();
41
process.env.MITM_ALLOW_INSECURE = 'true';
42
await tab.goto(url);
43
expect(rawHeaders[0]).toBe('Host');
44
process.env.MITM_ALLOW_INSECURE = 'false';
45
});
46
47
test('should send preflight requests', async () => {
48
const corsPromise = new Promise<boolean>(resolve => {
49
koa.options('/preflightPost', ctx => {
50
ctx.response.set('Access-Control-Allow-Origin', ctx.headers.origin);
51
ctx.response.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
52
ctx.response.set('Access-Control-Allow-Headers', 'X-PINGOTHER, Content-Type');
53
ctx.body = '';
54
resolve(true);
55
});
56
});
57
const postPromise = new Promise<boolean>(resolve => {
58
koa.post('/preflightPost', ctx => {
59
ctx.body = 'ok';
60
resolve(true);
61
});
62
});
63
64
const session = await GlobalPool.createSession({});
65
Helpers.needsClosing.push(session);
66
session.mitmRequestSession.blockedResources.urls = [
67
'http://dataliberationfoundation.org/postback',
68
];
69
session.mitmRequestSession.blockedResources.handlerFn = (request, response) => {
70
response.end(`<html lang="en">
71
<body>
72
<script type="text/javascript">
73
const xhr = new XMLHttpRequest();
74
xhr.open('POST', '${koa.baseUrl}/preflightPost');
75
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
76
xhr.setRequestHeader('Content-Type', 'application/xml');
77
xhr.send('<person><name>DLF</name></person>');
78
</script>
79
</body>
80
</html>
81
`);
82
return true;
83
};
84
const tab = await session.createTab();
85
await tab.goto(`http://dataliberationfoundation.org/postback`);
86
await expect(corsPromise).resolves.toBeTruthy();
87
await expect(postPromise).resolves.toBeTruthy();
88
89
expect(mocks.MitmRequestContext.create).toHaveBeenCalledTimes(3);
90
91
const context = mocks.MitmRequestContext.create.mock.results[1];
92
expect(context.value.method).toBe('OPTIONS');
93
94
const context2 = mocks.MitmRequestContext.create.mock.results[2];
95
expect(context2.value.method).toBe('POST');
96
});
97
98
test('should proxy requests from worker threads', async () => {
99
koa.get('/worker.js', ctx => {
100
ctx.set('content-type', 'application/javascript');
101
ctx.body = `
102
onmessage = function(e) {
103
const xhr = new XMLHttpRequest();
104
xhr.open('POST', '${koa.baseUrl}/xhr');
105
xhr.send('FromWorker');
106
}`;
107
});
108
koa.get('/testWorker', ctx => {
109
ctx.body = `<html lang="en">
110
<h1>This is a visible page</h1>
111
<script>
112
const myWorker = new Worker("worker.js");
113
myWorker.postMessage('send');
114
</script>
115
</html>
116
`;
117
});
118
const serviceXhr = new Promise<string>(resolve => {
119
koa.post('/xhr', async ctx => {
120
ctx.body = 'Ok';
121
const requestBody = await Helpers.readableToBuffer(ctx.req);
122
resolve(requestBody.toString());
123
});
124
});
125
const session = await GlobalPool.createSession({});
126
Helpers.needsClosing.push(session);
127
const tab = await session.createTab();
128
await tab.goto(`${koa.baseUrl}/testWorker`);
129
await tab.waitForLoad('PaintingStable');
130
await expect(serviceXhr).resolves.toBe('FromWorker');
131
expect(mocks.MitmRequestContext.create).toHaveBeenCalledTimes(3);
132
});
133
134
test('should proxy requests from shared workers', async () => {
135
const xhrResolvable = new Resolvable<string>();
136
const server = await Helpers.runHttpsServer(async (req, res) => {
137
if (req.url === '/shared-worker.js') {
138
res.setHeader('content-type', 'application/javascript');
139
res.end(`
140
onconnect = async message => {
141
const port = message.ports[0]
142
const xhr = new XMLHttpRequest();
143
xhr.open('POST', '${server.baseUrl}/sharedWorkerXhr');
144
xhr.send('FromSharedWorker');
145
port.postMessage('done')
146
}`);
147
} else if (req.url === '/testSharedWorker') {
148
res.setHeader('content-type', 'text/html');
149
res.end(`<html lang="en">
150
<script>
151
try {
152
153
const sharedWorker = new SharedWorker('./shared-worker.js');
154
sharedWorker.port.start()
155
sharedWorker.port.addEventListener('message', message => {
156
sharedWorker.port.close();
157
});
158
} catch(error ){
159
console.log('couldnt start shared worker', error)
160
}
161
</script>
162
<h1>This is a visible page</h1>
163
</html>
164
`);
165
} else if (req.url === '/sharedWorkerXhr') {
166
res.setHeader('content-type', 'text');
167
res.end('ok');
168
const requestBody = await Helpers.readableToBuffer(req);
169
xhrResolvable.resolve(requestBody.toString());
170
}
171
});
172
const session = await GlobalPool.createSession({});
173
Helpers.needsClosing.push(session);
174
const tab = await session.createTab();
175
await tab.goto(`${server.baseUrl}/testSharedWorker`);
176
await tab.waitForLoad('PaintingStable');
177
await expect(xhrResolvable.promise).resolves.toBe('FromSharedWorker');
178
expect(mocks.MitmRequestContext.create).toHaveBeenCalledTimes(3);
179
});
180
181
test('should not see proxy headers in a service worker', async () => {
182
const xhrHeaders = createPromise();
183
const xhrHeadersFromWorker = createPromise();
184
const server = await Helpers.runHttpsServer(async (request, response) => {
185
const path = request.url;
186
if (path === '/worker.js') {
187
response.setHeader('content-type', 'application/javascript');
188
response.end(`
189
self.addEventListener('fetch', event => {
190
event.respondWith(async function(){
191
return fetch(event.request.url, {
192
method: event.request.method,
193
credentials: 'include',
194
headers: {
195
'Intercepted': true,
196
'original-proxy-auth': event.request.headers['proxy-authorization']
197
},
198
body: event.request.headers['proxy-authorization'] ? 'ProxyAuth' : 'LooksGoodFromPage'
199
});
200
}());
201
});
202
203
self.addEventListener("install", (event) => {
204
event.waitUntil(self.skipWaiting());
205
});
206
207
self.addEventListener('activate', event => {
208
event.waitUntil(self.clients.claim());
209
});
210
211
self.addEventListener('message', event => {
212
if (event.data === 'activate-app') {
213
self.skipWaiting();
214
self.clients.claim();
215
self.clients.matchAll().then((clients) => {
216
clients.forEach((client) => client.postMessage("start-app"));
217
});
218
fetch('/xhr/2', {
219
method: 'POST',
220
body: 'FromWorker'
221
}).catch(err => {});
222
}
223
});
224
`);
225
}
226
if (path === '/service-worker') {
227
response.setHeader('Server-Worker-Allowed', '/xhr');
228
response.end(`<html lang="en"><body>
229
<h1>I'm loaded</h1>
230
<script>
231
232
window.addEventListener('load', function() {
233
navigator.serviceWorker.register('./worker.js');
234
navigator.serviceWorker.ready.then((reg) => {
235
if (reg.active) {
236
reg.active.postMessage("activate-app");
237
}
238
});
239
navigator.serviceWorker.addEventListener("message", (event) => {
240
if (event.data === 'start-app') {
241
fetch('/xhr', {
242
method: 'POST',
243
body: 'FromPage'
244
});
245
}
246
});
247
});
248
249
</script>
250
</body>
251
</html>
252
`);
253
}
254
255
let body = '';
256
for await (const chunk of request) body += chunk;
257
if (path === '/xhr') {
258
xhrHeaders.resolve(request.headers);
259
expect(body).toBe('LooksGoodFromPage');
260
response.end('Cool');
261
}
262
if (path === '/xhr/2') {
263
xhrHeadersFromWorker.resolve(request.headers);
264
expect(body).toBe('FromWorker');
265
response.end('Got it');
266
}
267
});
268
269
process.env.MITM_ALLOW_INSECURE = 'true';
270
const session = await GlobalPool.createSession({});
271
Helpers.needsClosing.push(session);
272
const tab = await session.createTab();
273
await tab.goto(`${server.baseUrl}/service-worker`);
274
await tab.waitForLoad('PaintingStable');
275
const [originalHeaders, headersFromWorker] = await Promise.all([
276
xhrHeaders.promise,
277
xhrHeadersFromWorker.promise,
278
]);
279
// check that both go through mitm
280
await expect(originalHeaders['proxy-authorization']).not.toBeTruthy();
281
await expect(headersFromWorker['proxy-authorization']).not.toBeTruthy();
282
await expect(originalHeaders['user-agent']).toBe(headersFromWorker['user-agent']);
283
expect(mocks.MitmRequestContext.create).toHaveBeenCalledTimes(4);
284
});
285
286
test('should proxy iframe requests', async () => {
287
const connection = Core.addConnection();
288
Helpers.onClose(() => connection.disconnect());
289
290
const meta = await connection.createSession();
291
const tab = Session.getTab(meta);
292
293
const session = tab.session;
294
295
session.mitmRequestSession.blockedResources.urls = [
296
'https://dataliberationfoundation.org/iframe',
297
'https://dataliberationfoundation.org/test.css',
298
'https://dataliberationfoundation.org/dlfSite.png',
299
];
300
session.mitmRequestSession.blockedResources.handlerFn = (request, response) => {
301
response.end(`<html lang="en">
302
<head><link rel="stylesheet" type="text/css" href="/test.css"/></head>
303
<body><img alt="none" src="/dlfSite.png"/></body>
304
</html>`);
305
return true;
306
};
307
koa.get('/iframe-test', async ctx => {
308
ctx.body = `<html lang="en">
309
<body>
310
This is the main body
311
<iframe src="https://dataliberationfoundation.org/iframe"></iframe>
312
</body>
313
</html>`;
314
});
315
await tab.goto(`${koa.baseUrl}/iframe-test`);
316
await tab.waitForLoad(LocationStatus.AllContentLoaded);
317
expect(mocks.MitmRequestContext.create).toHaveBeenCalledTimes(4);
318
const urls = mocks.MitmRequestContext.create.mock.results.map(x => x.value.url.href);
319
expect(urls).toEqual([
320
expect.stringMatching(/http:\/\/localhost:\d+\/iframe-test/),
321
'https://dataliberationfoundation.org/iframe',
322
'https://dataliberationfoundation.org/test.css',
323
'https://dataliberationfoundation.org/dlfSite.png',
324
]);
325
});
326
327