@zorilla/puppeteer-extra-plugin-stealth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +324 -0
  4. package/dist/evasions/README.md +13 -0
  5. package/dist/evasions/_template/README.md +18 -0
  6. package/dist/evasions/_template/index.d.ts +13 -0
  7. package/dist/evasions/_template/index.d.ts.map +1 -0
  8. package/dist/evasions/_template/index.js +26 -0
  9. package/dist/evasions/_template/index.js.map +1 -0
  10. package/dist/evasions/_template/package.json +5 -0
  11. package/dist/evasions/_utils/README.md +287 -0
  12. package/dist/evasions/_utils/index.d.ts +238 -0
  13. package/dist/evasions/_utils/index.d.ts.map +1 -0
  14. package/dist/evasions/_utils/index.js +588 -0
  15. package/dist/evasions/_utils/index.js.map +1 -0
  16. package/dist/evasions/_utils/withUtils.d.ts +12 -0
  17. package/dist/evasions/_utils/withUtils.d.ts.map +1 -0
  18. package/dist/evasions/_utils/withUtils.js +47 -0
  19. package/dist/evasions/_utils/withUtils.js.map +1 -0
  20. package/dist/evasions/chrome.app/README.md +16 -0
  21. package/dist/evasions/chrome.app/index.d.ts +11 -0
  22. package/dist/evasions/chrome.app/index.d.ts.map +1 -0
  23. package/dist/evasions/chrome.app/index.js +97 -0
  24. package/dist/evasions/chrome.app/index.js.map +1 -0
  25. package/dist/evasions/chrome.app/package.json +5 -0
  26. package/dist/evasions/chrome.csi/README.md +29 -0
  27. package/dist/evasions/chrome.csi/index.d.ts +25 -0
  28. package/dist/evasions/chrome.csi/index.d.ts.map +1 -0
  29. package/dist/evasions/chrome.csi/index.js +69 -0
  30. package/dist/evasions/chrome.csi/index.js.map +1 -0
  31. package/dist/evasions/chrome.csi/package.json +5 -0
  32. package/dist/evasions/chrome.loadTimes/README.md +27 -0
  33. package/dist/evasions/chrome.loadTimes/index.d.ts +23 -0
  34. package/dist/evasions/chrome.loadTimes/index.d.ts.map +1 -0
  35. package/dist/evasions/chrome.loadTimes/index.js +163 -0
  36. package/dist/evasions/chrome.loadTimes/index.js.map +1 -0
  37. package/dist/evasions/chrome.loadTimes/package.json +5 -0
  38. package/dist/evasions/chrome.runtime/README.md +32 -0
  39. package/dist/evasions/chrome.runtime/index.d.ts +14 -0
  40. package/dist/evasions/chrome.runtime/index.d.ts.map +1 -0
  41. package/dist/evasions/chrome.runtime/index.js +252 -0
  42. package/dist/evasions/chrome.runtime/index.js.map +1 -0
  43. package/dist/evasions/chrome.runtime/package.json +5 -0
  44. package/dist/evasions/chrome.runtime/staticData.json +41 -0
  45. package/dist/evasions/defaultArgs/README.md +17 -0
  46. package/dist/evasions/defaultArgs/index.d.ts +2 -0
  47. package/dist/evasions/defaultArgs/index.d.ts.map +1 -0
  48. package/dist/evasions/defaultArgs/index.js +43 -0
  49. package/dist/evasions/defaultArgs/index.js.map +1 -0
  50. package/dist/evasions/defaultArgs/package.json +5 -0
  51. package/dist/evasions/iframe.contentWindow/README.md +19 -0
  52. package/dist/evasions/iframe.contentWindow/index.d.ts +15 -0
  53. package/dist/evasions/iframe.contentWindow/index.d.ts.map +1 -0
  54. package/dist/evasions/iframe.contentWindow/index.js +132 -0
  55. package/dist/evasions/iframe.contentWindow/index.js.map +1 -0
  56. package/dist/evasions/iframe.contentWindow/package.json +5 -0
  57. package/dist/evasions/media.codecs/README.md +38 -0
  58. package/dist/evasions/media.codecs/index.d.ts +12 -0
  59. package/dist/evasions/media.codecs/index.d.ts.map +1 -0
  60. package/dist/evasions/media.codecs/index.js +89 -0
  61. package/dist/evasions/media.codecs/index.js.map +1 -0
  62. package/dist/evasions/media.codecs/package.json +5 -0
  63. package/dist/evasions/navigator.hardwareConcurrency/README.md +19 -0
  64. package/dist/evasions/navigator.hardwareConcurrency/index.d.ts +2 -0
  65. package/dist/evasions/navigator.hardwareConcurrency/index.d.ts.map +1 -0
  66. package/dist/evasions/navigator.hardwareConcurrency/index.js +45 -0
  67. package/dist/evasions/navigator.hardwareConcurrency/index.js.map +1 -0
  68. package/dist/evasions/navigator.hardwareConcurrency/package.json +5 -0
  69. package/dist/evasions/navigator.languages/README.md +17 -0
  70. package/dist/evasions/navigator.languages/index.d.ts +2 -0
  71. package/dist/evasions/navigator.languages/index.d.ts.map +1 -0
  72. package/dist/evasions/navigator.languages/index.js +44 -0
  73. package/dist/evasions/navigator.languages/index.js.map +1 -0
  74. package/dist/evasions/navigator.languages/package.json +5 -0
  75. package/dist/evasions/navigator.permissions/README.md +16 -0
  76. package/dist/evasions/navigator.permissions/index.d.ts +2 -0
  77. package/dist/evasions/navigator.permissions/index.d.ts.map +1 -0
  78. package/dist/evasions/navigator.permissions/index.js +66 -0
  79. package/dist/evasions/navigator.permissions/index.js.map +1 -0
  80. package/dist/evasions/navigator.permissions/package.json +5 -0
  81. package/dist/evasions/navigator.plugins/README.md +24 -0
  82. package/dist/evasions/navigator.plugins/data.json +48 -0
  83. package/dist/evasions/navigator.plugins/functionMocks.d.ts +9 -0
  84. package/dist/evasions/navigator.plugins/functionMocks.d.ts.map +1 -0
  85. package/dist/evasions/navigator.plugins/functionMocks.js +47 -0
  86. package/dist/evasions/navigator.plugins/functionMocks.js.map +1 -0
  87. package/dist/evasions/navigator.plugins/index.d.ts +19 -0
  88. package/dist/evasions/navigator.plugins/index.d.ts.map +1 -0
  89. package/dist/evasions/navigator.plugins/index.js +98 -0
  90. package/dist/evasions/navigator.plugins/index.js.map +1 -0
  91. package/dist/evasions/navigator.plugins/magicArray.d.ts +2 -0
  92. package/dist/evasions/navigator.plugins/magicArray.d.ts.map +1 -0
  93. package/dist/evasions/navigator.plugins/magicArray.js +145 -0
  94. package/dist/evasions/navigator.plugins/magicArray.js.map +1 -0
  95. package/dist/evasions/navigator.plugins/mimeTypes.d.ts +2 -0
  96. package/dist/evasions/navigator.plugins/mimeTypes.d.ts.map +1 -0
  97. package/dist/evasions/navigator.plugins/mimeTypes.js +18 -0
  98. package/dist/evasions/navigator.plugins/mimeTypes.js.map +1 -0
  99. package/dist/evasions/navigator.plugins/package.json +5 -0
  100. package/dist/evasions/navigator.plugins/plugins.d.ts +2 -0
  101. package/dist/evasions/navigator.plugins/plugins.d.ts.map +1 -0
  102. package/dist/evasions/navigator.plugins/plugins.js +18 -0
  103. package/dist/evasions/navigator.plugins/plugins.js.map +1 -0
  104. package/dist/evasions/navigator.vendor/README.md +36 -0
  105. package/dist/evasions/navigator.vendor/index.d.ts +2 -0
  106. package/dist/evasions/navigator.vendor/index.d.ts.map +1 -0
  107. package/dist/evasions/navigator.vendor/index.js +64 -0
  108. package/dist/evasions/navigator.vendor/index.js.map +1 -0
  109. package/dist/evasions/navigator.vendor/package.json +5 -0
  110. package/dist/evasions/navigator.webdriver/README.md +17 -0
  111. package/dist/evasions/navigator.webdriver/index.d.ts +13 -0
  112. package/dist/evasions/navigator.webdriver/index.d.ts.map +1 -0
  113. package/dist/evasions/navigator.webdriver/index.js +48 -0
  114. package/dist/evasions/navigator.webdriver/index.js.map +1 -0
  115. package/dist/evasions/navigator.webdriver/package.json +5 -0
  116. package/dist/evasions/sourceurl/README.md +17 -0
  117. package/dist/evasions/sourceurl/_fixtures/test.html +35 -0
  118. package/dist/evasions/sourceurl/index.d.ts +12 -0
  119. package/dist/evasions/sourceurl/index.d.ts.map +1 -0
  120. package/dist/evasions/sourceurl/index.js +82 -0
  121. package/dist/evasions/sourceurl/index.js.map +1 -0
  122. package/dist/evasions/sourceurl/package.json +5 -0
  123. package/dist/evasions/user-agent-override/README.md +53 -0
  124. package/dist/evasions/user-agent-override/index.d.ts +2 -0
  125. package/dist/evasions/user-agent-override/index.d.ts.map +1 -0
  126. package/dist/evasions/user-agent-override/index.js +206 -0
  127. package/dist/evasions/user-agent-override/index.js.map +1 -0
  128. package/dist/evasions/user-agent-override/package.json +5 -0
  129. package/dist/evasions/webgl.vendor/README.md +20 -0
  130. package/dist/evasions/webgl.vendor/index.d.ts +17 -0
  131. package/dist/evasions/webgl.vendor/index.d.ts.map +1 -0
  132. package/dist/evasions/webgl.vendor/index.js +57 -0
  133. package/dist/evasions/webgl.vendor/index.js.map +1 -0
  134. package/dist/evasions/webgl.vendor/package.json +5 -0
  135. package/dist/evasions/window.outerdimensions/README.md +17 -0
  136. package/dist/evasions/window.outerdimensions/index.d.ts +13 -0
  137. package/dist/evasions/window.outerdimensions/index.d.ts.map +1 -0
  138. package/dist/evasions/window.outerdimensions/index.js +42 -0
  139. package/dist/evasions/window.outerdimensions/index.js.map +1 -0
  140. package/dist/evasions/window.outerdimensions/package.json +5 -0
  141. package/dist/index.d.ts +130 -0
  142. package/dist/index.d.ts.map +1 -0
  143. package/dist/index.js +164 -0
  144. package/dist/index.js.map +1 -0
  145. package/examples/detect-headless.js +95 -0
  146. package/examples/test1.js +20 -0
  147. package/examples/test2.js +25 -0
  148. package/package.json +70 -0
  149. package/src/evasions/README.md +13 -0
  150. package/src/evasions/_template/README.md +18 -0
  151. package/src/evasions/_template/index.js +26 -0
  152. package/src/evasions/_template/package.json +5 -0
  153. package/src/evasions/_utils/README.md +287 -0
  154. package/src/evasions/_utils/index.js +588 -0
  155. package/src/evasions/_utils/withUtils.js +47 -0
  156. package/src/evasions/chrome.app/README.md +16 -0
  157. package/src/evasions/chrome.app/index.js +97 -0
  158. package/src/evasions/chrome.app/package.json +5 -0
  159. package/src/evasions/chrome.csi/README.md +29 -0
  160. package/src/evasions/chrome.csi/index.js +69 -0
  161. package/src/evasions/chrome.csi/package.json +5 -0
  162. package/src/evasions/chrome.loadTimes/README.md +27 -0
  163. package/src/evasions/chrome.loadTimes/index.js +163 -0
  164. package/src/evasions/chrome.loadTimes/package.json +5 -0
  165. package/src/evasions/chrome.runtime/README.md +32 -0
  166. package/src/evasions/chrome.runtime/index.js +252 -0
  167. package/src/evasions/chrome.runtime/package.json +5 -0
  168. package/src/evasions/chrome.runtime/staticData.json +41 -0
  169. package/src/evasions/defaultArgs/README.md +17 -0
  170. package/src/evasions/defaultArgs/index.js +43 -0
  171. package/src/evasions/defaultArgs/package.json +5 -0
  172. package/src/evasions/iframe.contentWindow/README.md +19 -0
  173. package/src/evasions/iframe.contentWindow/index.js +132 -0
  174. package/src/evasions/iframe.contentWindow/package.json +5 -0
  175. package/src/evasions/media.codecs/README.md +38 -0
  176. package/src/evasions/media.codecs/index.js +89 -0
  177. package/src/evasions/media.codecs/package.json +5 -0
  178. package/src/evasions/navigator.hardwareConcurrency/README.md +19 -0
  179. package/src/evasions/navigator.hardwareConcurrency/index.js +45 -0
  180. package/src/evasions/navigator.hardwareConcurrency/package.json +5 -0
  181. package/src/evasions/navigator.languages/README.md +17 -0
  182. package/src/evasions/navigator.languages/index.js +44 -0
  183. package/src/evasions/navigator.languages/package.json +5 -0
  184. package/src/evasions/navigator.permissions/README.md +16 -0
  185. package/src/evasions/navigator.permissions/index.js +66 -0
  186. package/src/evasions/navigator.permissions/package.json +5 -0
  187. package/src/evasions/navigator.plugins/README.md +24 -0
  188. package/src/evasions/navigator.plugins/data.json +48 -0
  189. package/src/evasions/navigator.plugins/functionMocks.js +47 -0
  190. package/src/evasions/navigator.plugins/index.js +98 -0
  191. package/src/evasions/navigator.plugins/magicArray.js +145 -0
  192. package/src/evasions/navigator.plugins/mimeTypes.js +18 -0
  193. package/src/evasions/navigator.plugins/package.json +5 -0
  194. package/src/evasions/navigator.plugins/plugins.js +18 -0
  195. package/src/evasions/navigator.vendor/README.md +36 -0
  196. package/src/evasions/navigator.vendor/index.js +64 -0
  197. package/src/evasions/navigator.vendor/package.json +5 -0
  198. package/src/evasions/navigator.webdriver/README.md +17 -0
  199. package/src/evasions/navigator.webdriver/index.js +48 -0
  200. package/src/evasions/navigator.webdriver/package.json +5 -0
  201. package/src/evasions/sourceurl/README.md +17 -0
  202. package/src/evasions/sourceurl/_fixtures/test.html +35 -0
  203. package/src/evasions/sourceurl/index.js +82 -0
  204. package/src/evasions/sourceurl/package.json +5 -0
  205. package/src/evasions/user-agent-override/README.md +53 -0
  206. package/src/evasions/user-agent-override/index.js +206 -0
  207. package/src/evasions/user-agent-override/package.json +5 -0
  208. package/src/evasions/webgl.vendor/README.md +20 -0
  209. package/src/evasions/webgl.vendor/index.js +57 -0
  210. package/src/evasions/webgl.vendor/package.json +5 -0
  211. package/src/evasions/window.outerdimensions/README.md +17 -0
  212. package/src/evasions/window.outerdimensions/index.js +42 -0
  213. package/src/evasions/window.outerdimensions/package.json +5 -0
  214. package/src/index.d.ts +111 -0
  215. package/src/index.ts +188 -0
  216. package/test/cat-and-mouse.test.ts +170 -0
  217. package/test/evasions/_utils/index.test.ts +710 -0
  218. package/test/evasions/chrome.app/index.test.ts +69 -0
  219. package/test/evasions/chrome.csi/index.test.ts +46 -0
  220. package/test/evasions/chrome.loadTimes/index.test.ts +61 -0
  221. package/test/evasions/chrome.runtime/index.test.ts +282 -0
  222. package/test/evasions/defaultArgs/index.test.ts +36 -0
  223. package/test/evasions/iframe.contentWindow/index.test.js +450 -0
  224. package/test/evasions/media.codecs/index.test.js +103 -0
  225. package/test/evasions/navigator.hardwareConcurrency/index.test.js +58 -0
  226. package/test/evasions/navigator.languages/index.test.js +101 -0
  227. package/test/evasions/navigator.permissions/index.test.js +104 -0
  228. package/test/evasions/navigator.plugins/index.test.js +55 -0
  229. package/test/evasions/navigator.plugins/mimeTypes.test.js +220 -0
  230. package/test/evasions/navigator.plugins/plugins.test.js +181 -0
  231. package/test/evasions/navigator.vendor/index.test.js +68 -0
  232. package/test/evasions/navigator.webdriver/index.test.js +47 -0
  233. package/test/evasions/sourceurl/_fixtures/test.html +35 -0
  234. package/test/evasions/sourceurl/index.test.js +62 -0
  235. package/test/evasions/user-agent-override/index.test.js +338 -0
  236. package/test/evasions/webgl.vendor/index.test.js +220 -0
  237. package/test/fixtures/dummy-with-service-worker.html +22 -0
  238. package/test/fixtures/dummy.html +11 -0
  239. package/test/fixtures/sw.js +1 -0
  240. package/test/fpscanner.test.ts +54 -0
  241. package/test/index.test.ts +51 -0
  242. package/test/service-worker.test.ts +112 -0
  243. package/test/stealth/_results/_thumbs/headful-chrome-stealth.js.png +0 -0
  244. package/test/stealth/_results/_thumbs/headful-chrome-vanilla.js.png +0 -0
  245. package/test/stealth/_results/_thumbs/headful-chromium-stealth.js.png +0 -0
  246. package/test/stealth/_results/_thumbs/headful-chromium-vanilla.js.png +0 -0
  247. package/test/stealth/_results/_thumbs/headless-chrome-stealth.js.png +0 -0
  248. package/test/stealth/_results/_thumbs/headless-chrome-vanilla.js.png +0 -0
  249. package/test/stealth/_results/_thumbs/headless-chromium-stealth.js.png +0 -0
  250. package/test/stealth/_results/_thumbs/headless-chromium-vanilla.js.png +0 -0
  251. package/test/stealth/_results/headful-chrome-stealth.js.png +0 -0
  252. package/test/stealth/_results/headful-chrome-vanilla.js.png +0 -0
  253. package/test/stealth/_results/headful-chromium-stealth.js.png +0 -0
  254. package/test/stealth/_results/headful-chromium-vanilla.js.png +0 -0
  255. package/test/stealth/_results/headless-chrome-stealth.js.png +0 -0
  256. package/test/stealth/_results/headless-chrome-vanilla.js.png +0 -0
  257. package/test/stealth/_results/headless-chromium-stealth.js.png +0 -0
  258. package/test/stealth/_results/headless-chromium-vanilla.js.png +0 -0
  259. package/test/stealth/headful-chrome-stealth.js +25 -0
  260. package/test/stealth/headful-chrome-vanilla.js +23 -0
  261. package/test/stealth/headful-chromium-stealth.js +22 -0
  262. package/test/stealth/headful-chromium-vanilla.js +20 -0
  263. package/test/stealth/headless-chrome-stealth.js +25 -0
  264. package/test/stealth/headless-chrome-vanilla.js +23 -0
  265. package/test/stealth/headless-chromium-stealth.js +22 -0
  266. package/test/stealth/headless-chromium-vanilla.js +20 -0
  267. package/test/util.js +82 -0
  268. package/tsconfig.json +10 -0
  269. package/tsconfig.tsbuildinfo +1 -0
  270. package/vitest.config.ts +28 -0
@@ -0,0 +1,710 @@
1
+ import { expect, test } from 'vitest';
2
+ import utils from '../../../src/evasions/_utils/index.js';
3
+ import withUtils from '../../../src/evasions/_utils/withUtils.js';
4
+ import { vanillaPuppeteer } from '../../util.js';
5
+
6
+ /* global HTMLMediaElement WebGLRenderingContext */
7
+
8
+ test('splitObjPath: will do what it says', async () => {
9
+ const { objName, propName } = utils.splitObjPath(
10
+ 'HTMLMediaElement.prototype.canPlayType'
11
+ );
12
+ expect(objName).toBe('HTMLMediaElement.prototype');
13
+ expect(propName).toBe('canPlayType');
14
+ });
15
+
16
+ test('makeNativeString: will do what it says', async () => {
17
+ utils.init();
18
+ expect(utils.makeNativeString('bob')).toBe(
19
+ 'function bob() { [native code] }'
20
+ );
21
+ expect(utils.makeNativeString('toString')).toBe(
22
+ 'function toString() { [native code] }'
23
+ );
24
+ expect(utils.makeNativeString()).toBe('function () { [native code] }');
25
+ });
26
+
27
+ test('replaceWithProxy: will work correctly', async () => {
28
+ const browser = await vanillaPuppeteer.launch({ headless: true });
29
+ const page = await browser.newPage();
30
+
31
+ const test1 = await withUtils(page).evaluate(utils => {
32
+ const dummyProxyHandler = {
33
+ get(_target, param) {
34
+ if (param && param === 'ping') {
35
+ return 'pong';
36
+ }
37
+ return utils.cache.Reflect.get(...(arguments || []));
38
+ },
39
+ apply() {
40
+ return utils.cache.Reflect.apply(...arguments);
41
+ },
42
+ };
43
+ utils.replaceWithProxy(
44
+ HTMLMediaElement.prototype,
45
+ 'canPlayType',
46
+ dummyProxyHandler
47
+ );
48
+ return {
49
+ toString: HTMLMediaElement.prototype.canPlayType.toString(),
50
+ ping: HTMLMediaElement.prototype.canPlayType.ping,
51
+ };
52
+ });
53
+ expect(test1).toEqual({
54
+ toString: 'function canPlayType() { [native code] }',
55
+ ping: 'pong',
56
+ });
57
+ });
58
+
59
+ test('replaceObjPathWithProxy: will work correctly', async () => {
60
+ const browser = await vanillaPuppeteer.launch({ headless: true });
61
+ const page = await browser.newPage();
62
+
63
+ const test1 = await withUtils(page).evaluate(utils => {
64
+ const dummyProxyHandler = {
65
+ get(_target, param) {
66
+ if (param && param === 'ping') {
67
+ return 'pong';
68
+ }
69
+ return utils.cache.Reflect.get(...(arguments || []));
70
+ },
71
+ apply() {
72
+ return utils.cache.Reflect.apply(...arguments);
73
+ },
74
+ };
75
+ utils.replaceObjPathWithProxy(
76
+ 'HTMLMediaElement.prototype.canPlayType',
77
+ dummyProxyHandler
78
+ );
79
+ return {
80
+ toString: HTMLMediaElement.prototype.canPlayType.toString(),
81
+ ping: HTMLMediaElement.prototype.canPlayType.ping,
82
+ };
83
+ });
84
+ expect(test1).toEqual({
85
+ toString: 'function canPlayType() { [native code] }',
86
+ ping: 'pong',
87
+ });
88
+ });
89
+
90
+ test('redirectToString: is battle hardened', async () => {
91
+ const browser = await vanillaPuppeteer.launch({ headless: true });
92
+ const page = await browser.newPage();
93
+
94
+ // Patch all documents including iframes
95
+ await withUtils(page).evaluateOnNewDocument(utils => {
96
+ // We redirect toString calls targeted at `canPlayType` to `getParameter`,
97
+ // so if everything works correctly we expect `getParameter` as response.
98
+ const proxyObj = HTMLMediaElement.prototype.canPlayType;
99
+ const originalObj = WebGLRenderingContext.prototype.getParameter;
100
+
101
+ utils.redirectToString(proxyObj, originalObj);
102
+ });
103
+ await page.goto('about:blank');
104
+
105
+ const result = await withUtils(page).evaluate(_utils => {
106
+ const iframe = document.createElement('iframe');
107
+ document.body.appendChild(iframe);
108
+
109
+ return {
110
+ target: {
111
+ raw: HTMLMediaElement.prototype.canPlayType + '',
112
+ rawiframe:
113
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType + '',
114
+ raw2: HTMLMediaElement.prototype.canPlayType.toString(),
115
+ rawiframe2:
116
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType.toString(),
117
+ direct: Function.prototype.toString.call(
118
+ HTMLMediaElement.prototype.canPlayType
119
+ ),
120
+ directWithiframe: iframe.contentWindow.Function.prototype.toString.call(
121
+ HTMLMediaElement.prototype.canPlayType
122
+ ),
123
+ iframeWithdirect: Function.prototype.toString.call(
124
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType
125
+ ),
126
+ iframeWithiframe: iframe.contentWindow.Function.prototype.toString.call(
127
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType
128
+ ),
129
+ },
130
+ toString: {
131
+ obj: HTMLMediaElement.prototype.canPlayType.toString + '',
132
+ objiframe:
133
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType.toString +
134
+ '',
135
+ raw: Function.prototype.toString + '',
136
+ rawiframe: iframe.contentWindow.Function.prototype.toString + '',
137
+ direct: Function.prototype.toString.call(Function.prototype.toString),
138
+ directWithiframe: iframe.contentWindow.Function.prototype.toString.call(
139
+ Function.prototype.toString
140
+ ),
141
+ iframeWithdirect: Function.prototype.toString.call(
142
+ iframe.contentWindow.Function.prototype.toString
143
+ ),
144
+ iframeWithiframe: iframe.contentWindow.Function.prototype.toString.call(
145
+ iframe.contentWindow.Function.prototype.toString
146
+ ),
147
+ },
148
+ };
149
+ });
150
+ expect(result).toEqual({
151
+ target: {
152
+ raw: 'function getParameter() { [native code] }',
153
+ raw2: 'function getParameter() { [native code] }',
154
+ rawiframe: 'function getParameter() { [native code] }',
155
+ rawiframe2: 'function getParameter() { [native code] }',
156
+ direct: 'function getParameter() { [native code] }',
157
+ directWithiframe: 'function getParameter() { [native code] }',
158
+ iframeWithdirect: 'function getParameter() { [native code] }',
159
+ iframeWithiframe: 'function getParameter() { [native code] }',
160
+ },
161
+ toString: {
162
+ obj: 'function toString() { [native code] }',
163
+ objiframe: 'function toString() { [native code] }',
164
+ raw: 'function toString() { [native code] }',
165
+ rawiframe: 'function toString() { [native code] }',
166
+ direct: 'function toString() { [native code] }',
167
+ directWithiframe: 'function toString() { [native code] }',
168
+ iframeWithdirect: 'function toString() { [native code] }',
169
+ iframeWithiframe: 'function toString() { [native code] }',
170
+ },
171
+ });
172
+ });
173
+
174
+ test('redirectToString: has proper errors', async () => {
175
+ const browser = await vanillaPuppeteer.launch({ headless: true });
176
+ const page = await browser.newPage();
177
+
178
+ // Patch all documents including iframes
179
+ await withUtils(page).evaluateOnNewDocument(utils => {
180
+ // We redirect toString calls targeted at `canPlayType` to `getParameter`,
181
+ // so if everything works correctly we expect `getParameter` as response.
182
+ const proxyObj = HTMLMediaElement.prototype.canPlayType;
183
+ const originalObj = WebGLRenderingContext.prototype.getParameter;
184
+
185
+ utils.redirectToString(proxyObj, originalObj);
186
+ });
187
+ await page.goto('about:blank');
188
+
189
+ const result = await withUtils(page).evaluate(_utils => {
190
+ const evalErr = (str = '') => {
191
+ try {
192
+ // eslint-disable-next-line no-eval
193
+ return eval(str);
194
+ } catch (err) {
195
+ return err.toString();
196
+ }
197
+ };
198
+
199
+ return {
200
+ blank: evalErr(`Function.prototype.toString.apply()`),
201
+ null: evalErr(`Function.prototype.toString.apply(null)`),
202
+ undef: evalErr(`Function.prototype.toString.apply(undefined)`),
203
+ emptyObject: evalErr(`Function.prototype.toString.apply({})`),
204
+ };
205
+ });
206
+ expect(result).toEqual({
207
+ blank:
208
+ "TypeError: Function.prototype.toString requires that 'this' be a Function",
209
+ null: "TypeError: Function.prototype.toString requires that 'this' be a Function",
210
+ undef:
211
+ "TypeError: Function.prototype.toString requires that 'this' be a Function",
212
+ emptyObject:
213
+ "TypeError: Function.prototype.toString requires that 'this' be a Function",
214
+ });
215
+ });
216
+
217
+ test('patchToString: will work correctly', async () => {
218
+ const browser = await vanillaPuppeteer.launch({ headless: true });
219
+ const page = await browser.newPage();
220
+
221
+ // Test verbatim string replacement
222
+ const test1 = await withUtils(page).evaluate(utils => {
223
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType, 'bob');
224
+ return HTMLMediaElement.prototype.canPlayType.toString();
225
+ });
226
+ expect(test1).toBe('bob');
227
+
228
+ // Test automatic mode derived from `.name`
229
+ const test2 = await withUtils(page).evaluate(utils => {
230
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType);
231
+ return HTMLMediaElement.prototype.canPlayType.toString();
232
+ });
233
+ expect(test2).toBe('function canPlayType() { [native code] }');
234
+
235
+ // Make sure automatic mode derived from `.name` works with proxies
236
+ const test3 = await withUtils(page).evaluate(utils => {
237
+ HTMLMediaElement.prototype.canPlayType = new Proxy(
238
+ HTMLMediaElement.prototype.canPlayType,
239
+ {}
240
+ );
241
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType);
242
+ return HTMLMediaElement.prototype.canPlayType.toString();
243
+ });
244
+ expect(test3).toBe('function canPlayType() { [native code] }');
245
+
246
+ // Actually verify there's an issue when using vanilla Proxies
247
+ const test4 = await withUtils(page).evaluate(_utils => {
248
+ HTMLMediaElement.prototype.canPlayType = new Proxy(
249
+ HTMLMediaElement.prototype.canPlayType,
250
+ {}
251
+ );
252
+ return HTMLMediaElement.prototype.canPlayType.toString();
253
+ });
254
+ expect(test4).toBe('function () { [native code] }');
255
+ });
256
+
257
+ function toStringTest(obj) {
258
+ obj = eval(obj); // eslint-disable-line no-eval
259
+ return `
260
+ - obj.toString(): ${obj.toString()}
261
+ - obj.name: ${obj.name}
262
+ - obj.toString + "": ${obj.toString + ''}
263
+ - obj.toString.name: ${obj.toString.name}
264
+ - obj.valueOf + "": ${obj.valueOf + ''}
265
+ - obj.valueOf().name: ${obj.valueOf().name}
266
+ - Object.prototype.toString.apply(obj): ${Object.prototype.toString.apply(obj)}
267
+ - Function.prototype.toString.call(obj): ${Function.prototype.toString.call(
268
+ obj
269
+ )}
270
+ - Function.prototype.valueOf.call(obj) + "": ${
271
+ Function.prototype.valueOf.call(obj) + ''
272
+ }
273
+ - obj.toString === Function.prototype.toString: ${
274
+ obj.toString === Function.prototype.toString
275
+ }
276
+ `.trim();
277
+ }
278
+
279
+ test('patchToString: passes all toString tests', async () => {
280
+ const toStringVanilla = await (async () => {
281
+ const browser = await vanillaPuppeteer.launch({ headless: true });
282
+ const page = await browser.newPage();
283
+ return page.evaluate(
284
+ toStringTest,
285
+ 'HTMLMediaElement.prototype.canPlayType'
286
+ );
287
+ })();
288
+ const toStringStealth = await (async () => {
289
+ const browser = await vanillaPuppeteer.launch({ headless: true });
290
+ const page = await browser.newPage();
291
+ await withUtils(page).evaluate(utils => {
292
+ HTMLMediaElement.prototype.canPlayType = function canPlayType() {};
293
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType);
294
+ });
295
+ return page.evaluate(
296
+ toStringTest,
297
+ 'HTMLMediaElement.prototype.canPlayType'
298
+ );
299
+ })();
300
+
301
+ // Check that the unmodified results are as expected
302
+ expect(toStringVanilla).toBe(
303
+ `
304
+ - obj.toString(): function canPlayType() { [native code] }
305
+ - obj.name: canPlayType
306
+ - obj.toString + "": function toString() { [native code] }
307
+ - obj.toString.name: toString
308
+ - obj.valueOf + "": function valueOf() { [native code] }
309
+ - obj.valueOf().name: canPlayType
310
+ - Object.prototype.toString.apply(obj): [object Function]
311
+ - Function.prototype.toString.call(obj): function canPlayType() { [native code] }
312
+ - Function.prototype.valueOf.call(obj) + "": function canPlayType() { [native code] }
313
+ - obj.toString === Function.prototype.toString: true
314
+ `.trim()
315
+ );
316
+
317
+ // Make sure our customizations leave no trace
318
+ expect(toStringVanilla).toBe(toStringStealth);
319
+ });
320
+
321
+ test('patchToString: passes stack trace tests', async () => {
322
+ const toStringStackTrace = () => {
323
+ try {
324
+ Object.create(
325
+ Object.getOwnPropertyDescriptor(Function.prototype, 'toString').get
326
+ ).toString();
327
+ } catch (err) {
328
+ return err.stack.split('\n').slice(0, 2).join('|');
329
+ }
330
+ return 'error not thrown';
331
+ };
332
+
333
+ const toStringVanilla = await (async () => {
334
+ const browser = await vanillaPuppeteer.launch({ headless: true });
335
+ const page = await browser.newPage();
336
+ return page.evaluate(toStringStackTrace);
337
+ })();
338
+ const toStringStealth = await (async () => {
339
+ const browser = await vanillaPuppeteer.launch({ headless: true });
340
+ const page = await browser.newPage();
341
+ await withUtils(page).evaluate(utils => {
342
+ HTMLMediaElement.prototype.canPlayType = function canPlayType() {};
343
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType);
344
+ });
345
+ return page.evaluate(toStringStackTrace);
346
+ })();
347
+
348
+ // Check that the unmodified results are as expected
349
+ expect(toStringVanilla).toBe(
350
+ `TypeError: Object prototype may only be an Object or null: undefined| at Object.create (<anonymous>)`.trim()
351
+ );
352
+
353
+ // Make sure our customizations leave no trace
354
+ expect(toStringVanilla).toBe(toStringStealth);
355
+ });
356
+
357
+ test('patchToString: vanilla has iframe issues', async () => {
358
+ const browser = await vanillaPuppeteer.launch({ headless: true });
359
+ const page = await browser.newPage();
360
+
361
+ // Only patch the main window
362
+ const result = await withUtils(page).evaluate(utils => {
363
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType, 'bob');
364
+
365
+ const iframe = document.createElement('iframe');
366
+ document.body.appendChild(iframe);
367
+ return {
368
+ direct: Function.prototype.toString.call(
369
+ HTMLMediaElement.prototype.canPlayType
370
+ ),
371
+ directWithiframe: iframe.contentWindow.Function.prototype.toString.call(
372
+ HTMLMediaElement.prototype.canPlayType
373
+ ),
374
+ iframeWithdirect: Function.prototype.toString.call(
375
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType
376
+ ),
377
+ iframeWithiframe: iframe.contentWindow.Function.prototype.toString.call(
378
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType
379
+ ),
380
+ };
381
+ });
382
+ expect(result).toEqual({
383
+ direct: 'bob',
384
+ directWithiframe: 'function canPlayType() { [native code] }',
385
+ iframeWithdirect: 'function canPlayType() { [native code] }',
386
+ iframeWithiframe: 'function canPlayType() { [native code] }',
387
+ });
388
+ });
389
+
390
+ test('patchToString: stealth has no iframe issues', async () => {
391
+ const browser = await vanillaPuppeteer.launch({ headless: true });
392
+ const page = await browser.newPage();
393
+
394
+ // Patch all documents including iframes
395
+ await withUtils(page).evaluateOnNewDocument(utils => {
396
+ utils.patchToString(HTMLMediaElement.prototype.canPlayType, 'alice');
397
+ });
398
+ await page.goto('about:blank');
399
+
400
+ const result = await withUtils(page).evaluate(_utils => {
401
+ const iframe = document.createElement('iframe');
402
+ document.body.appendChild(iframe);
403
+ return {
404
+ direct: Function.prototype.toString.call(
405
+ HTMLMediaElement.prototype.canPlayType
406
+ ),
407
+ directWithiframe: iframe.contentWindow.Function.prototype.toString.call(
408
+ HTMLMediaElement.prototype.canPlayType
409
+ ),
410
+ iframeWithdirect: Function.prototype.toString.call(
411
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType
412
+ ),
413
+ iframeWithiframe: iframe.contentWindow.Function.prototype.toString.call(
414
+ iframe.contentWindow.HTMLMediaElement.prototype.canPlayType
415
+ ),
416
+ };
417
+ });
418
+ expect(result).toEqual({
419
+ direct: 'alice',
420
+ directWithiframe: 'alice',
421
+ iframeWithdirect: 'alice',
422
+ iframeWithiframe: 'alice',
423
+ });
424
+ });
425
+
426
+ test('stripProxyFromErrors: will work correctly', async () => {
427
+ const browser = await vanillaPuppeteer.launch({ headless: true });
428
+ const page = await browser.newPage();
429
+
430
+ const results = await withUtils(page).evaluate(utils => {
431
+ const getStack = prop => {
432
+ try {
433
+ prop.caller(); // Will throw (HTMLMediaElement.prototype.canPlayType.caller)
434
+ return false;
435
+ } catch (err) {
436
+ return err.stack;
437
+ }
438
+ };
439
+ /** We need traps to show up in the error stack */
440
+ const dummyProxyHandler = {
441
+ get() {
442
+ return utils.cache.Reflect.get(...(arguments || []));
443
+ },
444
+ apply() {
445
+ return utils.cache.Reflect.apply(...arguments);
446
+ },
447
+ };
448
+ const vanillaProxy = new Proxy(
449
+ HTMLMediaElement.prototype.canPlayType,
450
+ dummyProxyHandler
451
+ );
452
+ const stealthProxy = new Proxy(
453
+ HTMLMediaElement.prototype.canPlayType,
454
+ utils.stripProxyFromErrors(dummyProxyHandler)
455
+ );
456
+
457
+ const stacks = {
458
+ vanilla: getStack(HTMLMediaElement.prototype.canPlayType),
459
+ vanillaProxy: getStack(vanillaProxy),
460
+ stealthProxy: getStack(stealthProxy),
461
+ };
462
+ return stacks;
463
+ });
464
+
465
+ // Check that the untouched stuff behaves as expected
466
+ expect(results.vanilla.includes(`TypeError: 'caller'`)).toBe(true);
467
+ expect(results.vanilla.includes(`at Object.get`)).toBe(false);
468
+
469
+ // Regression test: Make sure vanilla JS Proxies leak the stack trace
470
+ expect(results.vanillaProxy.includes(`TypeError: 'caller'`)).toBe(true);
471
+ expect(results.vanillaProxy.includes(`at Object.get`)).toBe(true);
472
+
473
+ // Stealth tests
474
+ expect(results.stealthProxy.includes(`TypeError: 'caller'`)).toBe(true);
475
+ expect(results.stealthProxy.includes(`at Object.get`)).toBe(false);
476
+ });
477
+
478
+ test('replaceProperty: will work without traces', async () => {
479
+ const browser = await vanillaPuppeteer.launch({ headless: true });
480
+ const page = await browser.newPage();
481
+
482
+ const results = await withUtils(page).evaluate(utils => {
483
+ utils.replaceProperty(Object.getPrototypeOf(navigator), 'languages', {
484
+ get: () => ['de-DE'],
485
+ });
486
+ return {
487
+ propNames: Object.getOwnPropertyNames(navigator),
488
+ };
489
+ });
490
+ expect(results.propNames.includes('languages')).toBe(false);
491
+ });
492
+
493
+ test('cache: will prevent leaks through overriding methods', async () => {
494
+ const browser = await vanillaPuppeteer.launch({ headless: true });
495
+ const page = await browser.newPage();
496
+
497
+ const results = await withUtils(page).evaluate(utils => {
498
+ const sniffResults = {
499
+ vanilla: false,
500
+ stealth: false,
501
+ };
502
+
503
+ const vanillaProxy = new Proxy(
504
+ {},
505
+ {
506
+ get() {
507
+ return Reflect.get(...arguments);
508
+ },
509
+ }
510
+ );
511
+ Reflect.get = () => (sniffResults.vanilla = true);
512
+ // trigger get trap
513
+ vanillaProxy.foo; // eslint-disable-line
514
+
515
+ const stealthProxy = new Proxy(
516
+ {},
517
+ {
518
+ get() {
519
+ return utils.cache.Reflect.get(...arguments); // using cached copy
520
+ },
521
+ }
522
+ );
523
+ Reflect.get = () => (sniffResults.stealth = true);
524
+ // trigger get trap
525
+ stealthProxy.foo; // eslint-disable-line
526
+
527
+ return sniffResults;
528
+ });
529
+
530
+ expect(results).toEqual({
531
+ vanilla: true,
532
+ stealth: false,
533
+ });
534
+ });
535
+
536
+ test('replaceWithProxy: will throw prototype errors', async () => {
537
+ const browser = await vanillaPuppeteer.launch({ headless: true });
538
+ const page = await browser.newPage();
539
+ await page.goto('about:blank');
540
+
541
+ const result = await withUtils(page).evaluate(utils => {
542
+ utils.replaceWithProxy(HTMLMediaElement.prototype, 'canPlayType', {});
543
+
544
+ const evalErr = (str = '') => {
545
+ try {
546
+ // eslint-disable-next-line no-eval
547
+ return eval(str);
548
+ } catch (err) {
549
+ return err.toString();
550
+ }
551
+ };
552
+
553
+ return {
554
+ same: evalErr(
555
+ `Object.setPrototypeOf(HTMLMediaElement.prototype.canPlayType, HTMLMediaElement.prototype.canPlayType) + ""`
556
+ ),
557
+ sameString: evalErr(
558
+ `Object.setPrototypeOf(Function.prototype.toString, Function.prototype.toString) + ""`
559
+ ),
560
+ null: evalErr(
561
+ `Object.setPrototypeOf(Function.prototype.toString, null) + ""`
562
+ ),
563
+ undef: evalErr(
564
+ `Object.setPrototypeOf(Function.prototype.toString, undefined) + ""`
565
+ ),
566
+ none: evalErr(`Object.setPrototypeOf(Function.prototype.toString) + ""`),
567
+ };
568
+ });
569
+ expect(result).toEqual({
570
+ same: 'TypeError: Cyclic __proto__ value',
571
+ sameString: 'TypeError: Cyclic __proto__ value',
572
+ null: 'TypeError: Cannot convert object to primitive value',
573
+ undef:
574
+ 'TypeError: Object prototype may only be an Object or null: undefined',
575
+ none: 'TypeError: Object prototype may only be an Object or null: undefined',
576
+ });
577
+ });
578
+
579
+ test('replaceGetterSetter', async () => {
580
+ const browser = await vanillaPuppeteer.launch({ headless: true });
581
+ const page = await browser.newPage();
582
+ await page.goto('about:blank');
583
+
584
+ const results = await withUtils(page).evaluate(utils => {
585
+ const getDetails = a => ({
586
+ href: a.href,
587
+ typeof: typeof a.href,
588
+ in: 'href' in a,
589
+ keys: Object.keys(a),
590
+ // eslint-disable-next-line no-undef
591
+ prototypeKeys: Object.keys(HTMLAnchorElement.prototype),
592
+ getOwnPropertyNames: Object.getOwnPropertyNames(a),
593
+ prototypeGetOwnPropertyNames: Object.getOwnPropertyNames(
594
+ // eslint-disable-next-line no-undef
595
+ HTMLAnchorElement.prototype
596
+ ),
597
+ ownPropertyDescriptor:
598
+ undefined === Object.getOwnPropertyDescriptor(a, 'href'),
599
+ prototypeOwnPropertyDescriptor: Object.getOwnPropertyDescriptor(
600
+ // eslint-disable-next-line no-undef
601
+ HTMLAnchorElement.prototype,
602
+ 'href'
603
+ ),
604
+ ownPropertyDescriptors: Object.getOwnPropertyDescriptors(a, 'href'),
605
+ prototypeOwnPropertyDescriptors: Object.getOwnPropertyDescriptors(
606
+ // eslint-disable-next-line no-undef
607
+ HTMLAnchorElement.prototype,
608
+ 'href'
609
+ ),
610
+ getToString: Object.getOwnPropertyDescriptor(
611
+ // eslint-disable-next-line no-undef
612
+ HTMLAnchorElement.prototype,
613
+ 'href'
614
+ ).get.toString(),
615
+ setToString: Object.getOwnPropertyDescriptor(
616
+ // eslint-disable-next-line no-undef
617
+ HTMLAnchorElement.prototype,
618
+ 'href'
619
+ ).set.toString(),
620
+ });
621
+
622
+ // Use native a.href.
623
+ const a1 = document.createElement('a');
624
+ a1.href = 'http://foo.com/';
625
+ const details1 = getDetails(a1);
626
+
627
+ // Override a.href.
628
+ let href = '';
629
+ // eslint-disable-next-line no-undef
630
+ utils.replaceGetterSetter(HTMLAnchorElement.prototype, 'href', {
631
+ get: () => href,
632
+ set: newValue => {
633
+ href = newValue;
634
+ },
635
+ });
636
+
637
+ // Use overrided a.href.
638
+ const a2 = document.createElement('a');
639
+ a2.href = 'http://foo.com/';
640
+ const details2 = getDetails(a2);
641
+
642
+ return [details1, details2];
643
+ });
644
+
645
+ expect(results[1]).toEqual(results[0]);
646
+ });
647
+
648
+ test('arrayEquals', async () => {
649
+ const browser = await vanillaPuppeteer.launch({ headless: true });
650
+ const page = await browser.newPage();
651
+ await page.goto('about:blank');
652
+
653
+ const results = await withUtils(page).evaluate(utils => {
654
+ const obj = { foo: 'bar' };
655
+ return {
656
+ a: utils.arrayEquals(['a', 'Alpha'], ['a', 'Alpha']),
657
+ b: !utils.arrayEquals(['b', 'Beta'], ['b', 'Blue']),
658
+ c: !utils.arrayEquals(['c', { foo: 'bar' }], ['c', { foo: 'bar' }]),
659
+ d: utils.arrayEquals(['d', obj], ['d', obj]),
660
+ e: utils.arrayEquals([null], [null]),
661
+ f: utils.arrayEquals([undefined], [undefined]),
662
+ g: utils.arrayEquals([false], [false]),
663
+ };
664
+ });
665
+
666
+ expect(results).toEqual({
667
+ a: true,
668
+ b: true,
669
+ c: true,
670
+ d: true,
671
+ e: true,
672
+ f: true,
673
+ g: true,
674
+ });
675
+ });
676
+
677
+ test('memoize', async () => {
678
+ const browser = await vanillaPuppeteer.launch({ headless: true });
679
+ const page = await browser.newPage();
680
+ await page.goto('about:blank');
681
+
682
+ const results = await withUtils(page).evaluate(utils => {
683
+ const objectify = utils.memoize((valueAdded, _valueIgnored) => {
684
+ return { valueAdded };
685
+ });
686
+
687
+ const obj = { foo: 'bar' };
688
+ /* eslint-disable no-self-compare */
689
+ return {
690
+ a: objectify('a', 'Alpha') === objectify('a', 'Alpha'),
691
+ b: objectify('b', 'Beta') !== objectify('b', 'Blue'),
692
+ c: objectify('c', { foo: 'bar' }) !== objectify('c', { foo: 'bar' }),
693
+ d: objectify('d', obj) === objectify('d', obj),
694
+ e: objectify(null) === objectify(null),
695
+ f: objectify(undefined) === objectify(undefined),
696
+ g: objectify(false) === objectify(false),
697
+ };
698
+ /* eslint-enable no-self-compare */
699
+ });
700
+
701
+ expect(results).toEqual({
702
+ a: true,
703
+ b: true,
704
+ c: true,
705
+ d: true,
706
+ e: true,
707
+ f: true,
708
+ g: true,
709
+ });
710
+ });