Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/full-client/test/detection.test.ts
1028 views
1
import { Helpers } from '@secret-agent/testing';
2
import * as Fs from 'fs';
3
import * as fpscanner from 'fpscanner';
4
import Core, { Session } from '@secret-agent/core';
5
import { ITestKoaServer } from '@secret-agent/testing/helpers';
6
import { Handler, LocationStatus } from '../index';
7
8
const fpCollectPath = require.resolve('fpcollect/src/fpCollect.js');
9
10
let handler: Handler;
11
let koaServer: ITestKoaServer;
12
beforeAll(async () => {
13
await Core.start();
14
handler = new Handler({ host: await Core.server.address });
15
Helpers.onClose(() => handler.close(), true);
16
koaServer = await Helpers.runKoaServer();
17
18
koaServer.get('/fpCollect.min.js', ctx => {
19
ctx.set('Content-Type', 'application/javascript');
20
ctx.body = Fs.readFileSync(fpCollectPath, 'utf-8')
21
.replace('module.exports = fpCollect;', '')
22
.replace('const fpCollect = ', 'var fpCollect = ');
23
});
24
koaServer.get('/collect', ctx => {
25
ctx.body = `
26
<body>
27
<h1>Collect test</h1>
28
<script src="/fpCollect.min.js"></script>
29
<script type="text/javascript">
30
(async () => {
31
fpCollect.addCustomFunction('detailChrome', false, () => {
32
const res = {};
33
34
["webstore", "runtime", "app", "csi", "loadTimes"].forEach((property) => {
35
try {
36
res[property] = window.chrome[property].constructor.toString();
37
} catch(e){
38
res.properties = e.toString();
39
}
40
});
41
42
try {
43
window.chrome.runtime.connect('');
44
} catch (e) {
45
res.connect = e.toString();
46
}
47
try {
48
window.chrome.runtime.sendMessage();
49
} catch (e) {
50
res.sendMessage = e.toString();
51
}
52
53
return res;
54
});
55
56
const fp = await fpCollect.generateFingerprint();
57
await fetch('/analyze', {
58
method:'POST',
59
body: JSON.stringify(fp),
60
});
61
})();
62
</script>
63
</body>
64
`;
65
});
66
});
67
afterAll(Helpers.afterAll, 30e3);
68
afterEach(Helpers.afterEach, 30e3);
69
70
test('widevine detection', async () => {
71
const agent = await handler.createAgent();
72
Helpers.needsClosing.push(agent);
73
await agent.goto(koaServer.baseUrl);
74
75
const accessKey = await agent
76
.getJsValue(
77
`navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{
78
initDataTypes: ['cenc'],
79
audioCapabilities: [
80
{
81
contentType: 'audio/mp4;codecs="mp4a.40.2"',
82
},
83
],
84
videoCapabilities: [
85
{
86
contentType: 'video/mp4;codecs="avc1.42E01E"',
87
},
88
],
89
},
90
]).then(x => {
91
if (x.keySystem !== 'com.widevine.alpha') throw new Error('Wrong keysystem ' + x.keySystem);
92
return x.createMediaKeys();
93
}).then(x => {
94
return x.constructor.name
95
})`,
96
)
97
.catch(err => err);
98
expect(accessKey).toBe('MediaKeys');
99
});
100
101
test('plays m3u8', async () => {
102
const agent = await handler.createAgent();
103
Helpers.needsClosing.push(agent);
104
await agent.goto(koaServer.baseUrl);
105
106
const isSupported = await agent
107
.getJsValue(`MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')`)
108
.catch(err => err);
109
expect(isSupported).toBe(true);
110
});
111
112
test('should pass FpScanner', async () => {
113
const analyzePromise = new Promise(resolve => {
114
koaServer.post('/analyze', async ctx => {
115
let body = '';
116
for await (const chunk of ctx.req) {
117
body += chunk.toString();
118
}
119
120
resolve(JSON.parse(body));
121
ctx.body = 'Ok';
122
});
123
});
124
125
const agent = await handler.createAgent();
126
Helpers.needsClosing.push(agent);
127
await agent.goto(`${koaServer.baseUrl}/collect`);
128
129
const data = await analyzePromise;
130
const results = fpscanner.analyseFingerprint(data);
131
for (const key of Object.keys(results)) {
132
const result = results[key];
133
const isConsistent = result.consistent === fpscanner.CONSISTENT;
134
// eslint-disable-next-line no-console
135
if (!isConsistent) console.log('Not consistent', result);
136
expect(isConsistent).toBe(true);
137
}
138
expect(data).toBeTruthy();
139
}, 30e3);
140
141
test('should not be denied for notifications but prompt for permissions', async () => {
142
const agent = await handler.createAgent();
143
Helpers.needsClosing.push(agent);
144
await agent.goto(`${koaServer.baseUrl}`);
145
const activeTab = await agent.activeTab;
146
const tabId = await activeTab.tabId;
147
const sessionId = await agent.sessionId;
148
const tab = Session.getTab({ tabId, sessionId });
149
const page = tab.puppetPage;
150
const permissions = await page.evaluate<any>(`(async () => {
151
const permissionStatus = await navigator.permissions.query({
152
name: 'notifications',
153
});
154
155
return {
156
notificationValue: Notification.permission,
157
permissionState: permissionStatus.state
158
}
159
})();`);
160
161
expect(permissions.notificationValue).toBe('default');
162
expect(permissions.permissionState).toBe('prompt');
163
});
164
165
test('should not leave markers on permissions.query.toString', async () => {
166
const agent = await handler.createAgent();
167
Helpers.needsClosing.push(agent);
168
const tabId = await agent.activeTab.tabId;
169
await agent.goto(`${koaServer.baseUrl}`);
170
const sessionId = await agent.sessionId;
171
const tab = Session.getTab({ tabId, sessionId });
172
const page = tab.puppetPage;
173
const perms: any = await page.evaluate(`(() => {
174
const permissions = window.navigator.permissions;
175
return {
176
hasDirectQueryProperty: permissions.hasOwnProperty('query'),
177
queryToString: permissions.query.toString(),
178
queryToStringToString: permissions.query.toString.toString(),
179
queryToStringHasProxyHandler: permissions.query.toString.hasOwnProperty('[[Handler]]'),
180
queryToStringHasProxyTarget: permissions.query.toString.hasOwnProperty('[[Target]]'),
181
queryToStringHasProxyRevoked: permissions.query.toString.hasOwnProperty('[[IsRevoked]]'),
182
}
183
})();`);
184
expect(perms.hasDirectQueryProperty).toBe(false);
185
expect(perms.queryToString).toBe('function query() { [native code] }');
186
expect(perms.queryToStringToString).toBe('function toString() { [native code] }');
187
expect(perms.queryToStringHasProxyHandler).toBe(false);
188
expect(perms.queryToStringHasProxyTarget).toBe(false);
189
expect(perms.queryToStringHasProxyRevoked).toBe(false);
190
});
191
192
test('should not recurse the toString function', async () => {
193
const agent = await handler.createAgent();
194
Helpers.needsClosing.push(agent);
195
await agent.goto(`${koaServer.baseUrl}`);
196
const tabId = await agent.activeTab.tabId;
197
const sessionId = await agent.sessionId;
198
const tab = Session.getTab({ tabId, sessionId });
199
const page = tab.puppetPage;
200
const isHeadless = await page.evaluate(`(() => {
201
let gotYou = 0;
202
const spooky = /./;
203
spooky.toString = function() {
204
gotYou += 1;
205
return 'spooky';
206
};
207
console.debug(spooky);
208
return gotYou > 1;
209
})();`);
210
expect(isHeadless).toBe(false);
211
});
212
213
test('should properly maintain stack traces in toString', async () => {
214
const agent = await handler.createAgent();
215
Helpers.needsClosing.push(agent);
216
await agent.goto(`${koaServer.baseUrl}`);
217
const tabId = await agent.activeTab.tabId;
218
const sessionId = await agent.sessionId;
219
const tab = Session.getTab({ tabId, sessionId });
220
const page = tab.puppetPage;
221
await page.evaluate(`(() => {
222
window.hasProperStackTrace = apiFunction => {
223
try {
224
Object.create(apiFunction).toString(); // native throws an error
225
return { stack: "Didn't Throw" };
226
} catch (error) {
227
return {
228
stack: error.stack,
229
name: error.constructor.name
230
};
231
}
232
};
233
})();`);
234
235
const fnStack = await page.evaluate<any>(
236
`window.hasProperStackTrace(Function.prototype.toString)`,
237
);
238
expect(fnStack.stack.split('\n').length).toBeGreaterThan(1);
239
expect(fnStack.name).toBe('TypeError');
240
expect(fnStack.stack.split('\n')[1]).toContain('at Function.toString');
241
242
const fnStack2 = await page.evaluate<any>(`window.hasProperStackTrace(() => {})`);
243
expect(fnStack2.stack.split('\n').length).toBeGreaterThan(1);
244
expect(fnStack2.name).toBe('TypeError');
245
expect(fnStack2.stack.split('\n')[1]).toContain('at Function.toString');
246
});
247
248
// https://github.com/digitalhurricane-io/puppeteer-detection-100-percent
249
test('should not leave stack trace markers when calling getJsValue', async () => {
250
const agent = await handler.createAgent();
251
Helpers.needsClosing.push(agent);
252
const tabId = await agent.activeTab.tabId;
253
await agent.goto(koaServer.baseUrl);
254
const sessionId = await agent.sessionId;
255
const tab = Session.getTab({ tabId, sessionId });
256
const page = tab.puppetPage;
257
await page.evaluate(`(() => {
258
document.querySelector = (function (orig) {
259
return function() {
260
const err = new Error('QuerySelector Override Detection');
261
return err.stack.toString();
262
};
263
})(document.querySelector);
264
})();`);
265
266
// for live variables, we shouldn't see markers of utils.js
267
const query = await tab.getJsValue('document.querySelector("h1")');
268
expect(query).toBe(
269
'Error: QuerySelector Override Detection\n at HTMLDocument.querySelector (<anonymous>:4:17)\n at <anonymous>:1:10',
270
);
271
});
272
273
test('should not leave stack trace markers when calling in page functions', async () => {
274
const agent = await handler.createAgent();
275
Helpers.needsClosing.push(agent);
276
koaServer.get('/marker', ctx => {
277
ctx.body = `
278
<body>
279
<h1>Marker Page</h1>
280
<script type="text/javascript">
281
function errorCheck() {
282
const err = new Error('This is from inside');
283
return err.stack.toString();
284
}
285
document.querySelectorAll = (function () {
286
return function outerFunction() {
287
const err = new Error('All Error');
288
return err.stack.toString();
289
};
290
})(document.querySelectorAll);
291
</script>
292
</body>
293
`;
294
});
295
const url = `${koaServer.baseUrl}/marker`;
296
await agent.goto(url);
297
await agent.waitForPaintingStable();
298
const tabId = await agent.activeTab.tabId;
299
const sessionId = await agent.sessionId;
300
const tab = Session.getTab({ tabId, sessionId });
301
302
const pageFunction = await tab.getJsValue('errorCheck()');
303
expect(pageFunction).toBe(
304
`Error: This is from inside\n at errorCheck (${url}:6:17)\n at <anonymous>:1:1`,
305
);
306
307
// for something created
308
const queryAllTest = await tab.getJsValue('document.querySelectorAll("h1")');
309
expect(queryAllTest).toBe(
310
`Error: All Error\n at HTMLDocument.outerFunction [as querySelectorAll] (${url}:11:19)\n at <anonymous>:1:10`,
311
);
312
});
313
314
test('should not have too much recursion in prototype', async () => {
315
const agent = await handler.createAgent();
316
Helpers.needsClosing.push(agent);
317
const tabId = await agent.activeTab.tabId;
318
const sessionId = await agent.sessionId;
319
const tab = Session.getTab({ tabId, sessionId });
320
const page = tab.puppetPage;
321
await agent.goto(`${koaServer.baseUrl}`);
322
await agent.activeTab.waitForLoad(LocationStatus.AllContentLoaded);
323
324
const error = await page.evaluate<{ message: string; name: string }>(`(() => {
325
const apiFunction = Object.getOwnPropertyDescriptor(Navigator.prototype, 'deviceMemory').get;
326
327
try {
328
Object.setPrototypeOf(apiFunction, apiFunction) + ''
329
return true
330
} catch (error) {
331
console.log(error)
332
return {
333
name: error.constructor.name,
334
message: error.message,
335
stack: error.stack,
336
}
337
}
338
})();`);
339
340
expect(error.name).toBe('TypeError');
341
342
const error2 = await page.evaluate<{ message: string; name: string }>(`(() => {
343
const apiFunction = WebGL2RenderingContext.prototype.getParameter;
344
345
try {
346
Object.setPrototypeOf(apiFunction, apiFunction) + ''
347
return true
348
} catch (error) {
349
console.log(error)
350
return {
351
name: error.constructor.name,
352
message: error.message,
353
stack: error.stack,
354
}
355
}
356
})();`);
357
358
expect(error2.name).toBe('TypeError');
359
});
360
361