├── .gitignore ├── LICENSE ├── README.md ├── examples ├── cloudflare.py ├── quick_started.py └── sannysoft.py ├── pyproject.toml ├── requirements.txt ├── setup.py └── undetected_playwright ├── __init__.py ├── ninja.py ├── puppeteer-extra-plugin-stealth ├── LICENSE └── evasions │ ├── _template │ ├── index.js │ └── package.json │ ├── _utils │ ├── index.js │ └── withUtils.js │ ├── chrome.app │ ├── index.js │ └── package.json │ ├── chrome.csi │ ├── index.js │ └── package.json │ ├── chrome.loadTimes │ ├── index.js │ └── package.json │ ├── chrome.runtime │ ├── index.js │ ├── package.json │ └── staticData.json │ ├── defaultArgs │ ├── index.js │ └── package.json │ ├── iframe.contentWindow │ ├── index.js │ └── package.json │ ├── media.codecs │ ├── index.js │ └── package.json │ ├── navigator.hardwareConcurrency │ ├── index.js │ └── package.json │ ├── navigator.languages │ ├── index.js │ └── package.json │ ├── navigator.permissions │ ├── index.js │ └── package.json │ ├── navigator.plugins │ ├── data.json │ ├── functionMocks.js │ ├── index.js │ ├── magicArray.js │ ├── mimeTypes.js │ ├── package.json │ └── plugins.js │ ├── navigator.vendor │ ├── index.js │ └── package.json │ ├── navigator.webdriver │ ├── index.js │ └── package.json │ ├── sourceurl │ ├── _fixtures │ │ └── test.html │ ├── index.js │ └── package.json │ ├── user-agent-override │ ├── index.js │ └── package.json │ ├── webgl.vendor │ ├── index.js │ └── package.json │ └── window.outerdimensions │ ├── index.js │ └── package.json └── tarnished.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea 131 | examples/result 132 | compile.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # undetected-playwright 2 | 3 | ## Usage 4 | 5 | 1. **Download PyPi package** 6 | 7 | ```bash 8 | pip install -U undetected-playwright 9 | ``` 10 | 11 | 2. **Donload dependencies** 12 | 13 | ```bash 14 | playwright install --with-deps 15 | ``` 16 | 17 | 3. **Quick started** 18 | 19 | ```python 20 | import webbrowser 21 | from datetime import datetime 22 | from pathlib import Path 23 | 24 | from playwright.sync_api import sync_playwright, Page 25 | 26 | from undetected_playwright import Tarnished 27 | 28 | 29 | def cache_screenshot(page: Page): 30 | _now = datetime.now().strftime("%Y-%m-%d") 31 | _suffix = f"-view-new-{datetime.now().strftime('%H%M%S')}" 32 | path = f"result/{_now}/sannysoft{_suffix}.png" 33 | page.screenshot(path=path, full_page=True) 34 | 35 | webbrowser.open(f"file://{Path(path).resolve()}") 36 | 37 | 38 | def main(): 39 | # Chrome 112+ 40 | args = ["--headless=new", "--dump-dom"] 41 | 42 | with sync_playwright() as p: 43 | browser = p.chromium.launch(args=args) 44 | context = browser.new_context(locale="en-US") 45 | 46 | # Injecting Context 47 | Tarnished.apply_stealth(context) 48 | page = context.new_page() 49 | 50 | page.goto("https://bot.sannysoft.com/", wait_until="networkidle") 51 | cache_screenshot(page) 52 | 53 | browser.close() 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | 59 | ``` 60 | 61 | ## How to 62 | 63 | ### Sync 64 | 65 | ```diff 66 | from playwright.sync_api import sync_playwright, Page 67 | + from undetected_playwright import Tarnished 68 | 69 | with sync_playwright() as p: 70 | browser = p.chromium.launch() 71 | context = browser.new_context() 72 | + Tarnished.apply_stealth(context) 73 | page = context.new_page() 74 | ``` 75 | 76 | ### Async 77 | 78 | ```diff 79 | from playwright.sync_api import sync_playwright, Page 80 | + from undetected_playwright import Malenia 81 | 82 | async def main(): 83 | async with async_playwright() as p: 84 | browser = await p.chromium.launch() 85 | context = await browser.new_context() 86 | + await Malenia.apply_stealth(context) 87 | page = await context.new_page() 88 | ``` 89 | 90 | ## Demo: SyncPlaywright Sannysoft 91 | 92 | ```python 93 | import logging 94 | import sys 95 | from datetime import datetime 96 | from enum import Enum 97 | 98 | from playwright.sync_api import sync_playwright, Page, Route 99 | 100 | from undetected_playwright import Tarnished 101 | 102 | logging.basicConfig( 103 | level=logging.DEBUG, stream=sys.stdout, format="%(asctime)s - %(levelname)s - %(message)s" 104 | ) 105 | 106 | 107 | class ViewMode(str, Enum): 108 | new = "new" 109 | headless = "headless" 110 | headful = "headful" 111 | 112 | 113 | def _hijacker(route: Route): 114 | logging.debug(f"{route.request.method} {route.request.url}") 115 | route.continue_() 116 | 117 | 118 | def worker(page: Page, view_mode: ViewMode | None = None): 119 | logging.info(f"Worker started - {view_mode=}") 120 | 121 | page.route("**/*", _hijacker) 122 | page.goto("https://bot.sannysoft.com/", wait_until="networkidle") 123 | 124 | # Save screenshot 125 | _now = datetime.now().strftime("%Y-%m-%d") 126 | if view_mode: 127 | _suffix = f"-view-{view_mode}" 128 | else: 129 | _suffix = f"-view-{datetime.now().strftime('%H%M%S')}" 130 | page.screenshot(path=f"result/{_now}/sannysoft{_suffix}.png", full_page=True) 131 | 132 | logging.info(f"Worker finished - {view_mode=}") 133 | 134 | 135 | def bytedance(view_mode: ViewMode): 136 | with sync_playwright() as p: 137 | match view_mode: 138 | case "new": 139 | args = ["--headless=new", "--dump-dom"] 140 | browser = p.chromium.launch(args=args) 141 | case "headless": 142 | browser = p.chromium.launch(headless=True) 143 | case _: 144 | browser = p.chromium.launch(headless=False) 145 | 146 | context = browser.new_context(locale="en-US") 147 | Tarnished.apply_stealth(context) 148 | 149 | page = context.new_page() 150 | worker(page, view_mode) 151 | 152 | browser.close() 153 | 154 | 155 | def main(): 156 | bytedance(ViewMode.new) 157 | bytedance(ViewMode.headful) 158 | bytedance(ViewMode.headless) 159 | 160 | 161 | if __name__ == "__main__": 162 | main() 163 | 164 | ``` 165 | 166 | ## Demo: AsyncPlaywright CloudFlare 167 | 168 | ```python 169 | import asyncio 170 | import logging 171 | import sys 172 | from datetime import datetime 173 | from enum import Enum 174 | 175 | from playwright.async_api import async_playwright, Page, Route 176 | 177 | from undetected_playwright import Malenia 178 | 179 | logging.basicConfig( 180 | level=logging.DEBUG, stream=sys.stdout, format="%(asctime)s - %(levelname)s - %(message)s" 181 | ) 182 | 183 | 184 | class ViewMode(str, Enum): 185 | new = "new" 186 | headless = "headless" 187 | headful = "headful" 188 | 189 | 190 | async def _hijacker(route: Route): 191 | logging.debug(f"{route.request.method} {route.request.url}") 192 | await route.continue_() 193 | 194 | 195 | async def worker(page: Page, view_mode: ViewMode | None = None): 196 | logging.info(f"Worker started - {view_mode=}") 197 | 198 | await page.route("**/*", _hijacker) 199 | await page.goto("https://www.nowsecure.nl", wait_until="networkidle") 200 | 201 | # Just for demo 202 | await page.wait_for_timeout(8000) 203 | 204 | # Save screenshot 205 | _now = datetime.now().strftime("%Y-%m-%d") 206 | if view_mode: 207 | _suffix = f"-view-{view_mode}" 208 | else: 209 | _suffix = f"-view-{datetime.now().strftime('%H%M%S')}" 210 | await page.screenshot(path=f"result/{_now}/cloudflare{_suffix}.png", full_page=True) 211 | 212 | logging.info(f"Worker finished - {view_mode=}") 213 | 214 | 215 | async def bytedance(view_mode: ViewMode): 216 | async with async_playwright() as p: 217 | match view_mode: 218 | case "new": 219 | args = ["--headless=new", "--dump-dom"] 220 | browser = await p.chromium.launch(args=args) 221 | case "headless": 222 | browser = await p.chromium.launch(headless=True) 223 | case _: 224 | browser = await p.chromium.launch(headless=False) 225 | 226 | context = await browser.new_context(locale="en-US") 227 | await Malenia.apply_stealth(context) 228 | 229 | page = await context.new_page() 230 | await worker(page, view_mode) 231 | 232 | await browser.close() 233 | 234 | 235 | async def main(): 236 | await bytedance(ViewMode.new) 237 | await bytedance(ViewMode.headful) 238 | await bytedance(ViewMode.headless) 239 | 240 | 241 | if __name__ == "__main__": 242 | asyncio.run(main()) 243 | 244 | ``` 245 | 246 | ## Reference 247 | 248 | - [berstend/puppeteer-extra](https://github.com/berstend/puppeteer-extra) 249 | - [AtuboDad/playwright_stealth: playwright stealth (github.com)](https://github.com/AtuboDad/playwright_stealth) 250 | - [Granitosaurus/playwright-stealth (github.com)](https://github.com/Granitosaurus/playwright-stealth) 251 | - [diprajpatra/selenium-stealth: Trying to make python selenium more stealthy. (github.com)](https://github.com/diprajpatra/selenium-stealth) 252 | -------------------------------------------------------------------------------- /examples/cloudflare.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2023/8/24 20:44 3 | # Author : QIN2DIM 4 | # GitHub : https://github.com/QIN2DIM 5 | # Description: 6 | import asyncio 7 | import logging 8 | import sys 9 | from datetime import datetime 10 | from enum import Enum 11 | 12 | from playwright.async_api import async_playwright, Page, Route 13 | 14 | from undetected_playwright import Malenia 15 | 16 | logging.basicConfig( 17 | level=logging.DEBUG, stream=sys.stdout, format="%(asctime)s - %(levelname)s - %(message)s" 18 | ) 19 | 20 | 21 | class ViewMode(str, Enum): 22 | new = "new" 23 | headless = "headless" 24 | headful = "headful" 25 | 26 | 27 | async def _hijacker(route: Route): 28 | logging.debug(f"{route.request.method} {route.request.url}") 29 | await route.continue_() 30 | 31 | 32 | async def worker(page: Page, view_mode: ViewMode | None = None): 33 | logging.info(f"Worker started - {view_mode=}") 34 | 35 | await page.route("**/*", _hijacker) 36 | await page.goto("https://www.nowsecure.nl", wait_until="networkidle") 37 | 38 | # Just for demo 39 | await page.wait_for_timeout(8000) 40 | 41 | # Save screenshot 42 | _now = datetime.now().strftime("%Y-%m-%d") 43 | if view_mode: 44 | _suffix = f"-view-{view_mode}" 45 | else: 46 | _suffix = f"-view-{datetime.now().strftime('%H%M%S')}" 47 | await page.screenshot(path=f"result/{_now}/cloudflare{_suffix}.png", full_page=True) 48 | 49 | logging.info(f"Worker finished - {view_mode=}") 50 | 51 | 52 | async def bytedance(view_mode: ViewMode): 53 | async with async_playwright() as p: 54 | match view_mode: 55 | case "new": 56 | args = ["--headless=new", "--dump-dom"] 57 | browser = await p.chromium.launch(args=args) 58 | case "headless": 59 | browser = await p.chromium.launch(headless=True) 60 | case _: 61 | browser = await p.chromium.launch(headless=False) 62 | 63 | context = await browser.new_context(locale="en-US") 64 | await Malenia.apply_stealth(context) 65 | 66 | page = await context.new_page() 67 | await worker(page, view_mode) 68 | 69 | await browser.close() 70 | 71 | 72 | async def main(): 73 | await bytedance(ViewMode.new) 74 | await bytedance(ViewMode.headful) 75 | await bytedance(ViewMode.headless) 76 | 77 | 78 | if __name__ == "__main__": 79 | asyncio.run(main()) 80 | -------------------------------------------------------------------------------- /examples/quick_started.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2024/5/19 9:52 3 | # Author : QIN2DIM 4 | # GitHub : https://github.com/QIN2DIM 5 | # Description: 6 | import webbrowser 7 | from datetime import datetime 8 | from pathlib import Path 9 | 10 | from playwright.sync_api import sync_playwright, Page 11 | 12 | from undetected_playwright import Tarnished 13 | 14 | 15 | def cache_screenshot(page: Page): 16 | _now = datetime.now().strftime("%Y-%m-%d") 17 | _suffix = f"-view-new-{datetime.now().strftime('%H%M%S')}" 18 | path = f"result/{_now}/sannysoft{_suffix}.png" 19 | page.screenshot(path=path, full_page=True) 20 | 21 | webbrowser.open(f"file://{Path(path).resolve()}") 22 | 23 | 24 | def main(): 25 | # Chrome 112+ 26 | args = ["--headless=new", "--dump-dom"] 27 | 28 | with sync_playwright() as p: 29 | browser = p.chromium.launch(args=args) 30 | context = browser.new_context(locale="en-US") 31 | 32 | # Injecting Context 33 | Tarnished.apply_stealth(context) 34 | page = context.new_page() 35 | 36 | page.goto("https://bot.sannysoft.com/", wait_until="networkidle") 37 | cache_screenshot(page) 38 | 39 | browser.close() 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /examples/sannysoft.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2022/10/24 13:23 3 | # Author : QIN2DIM 4 | # GitHub : https://github.com/QIN2DIM 5 | # Description: 6 | import logging 7 | import sys 8 | from datetime import datetime 9 | from enum import Enum 10 | 11 | from playwright.sync_api import sync_playwright, Page, Route 12 | 13 | from undetected_playwright import Tarnished 14 | 15 | logging.basicConfig( 16 | level=logging.DEBUG, stream=sys.stdout, format="%(asctime)s - %(levelname)s - %(message)s" 17 | ) 18 | 19 | 20 | class ViewMode(str, Enum): 21 | new = "new" 22 | headless = "headless" 23 | headful = "headful" 24 | 25 | 26 | def _hijacker(route: Route): 27 | logging.debug(f"{route.request.method} {route.request.url}") 28 | route.continue_() 29 | 30 | 31 | def worker(page: Page, view_mode: ViewMode | None = None): 32 | logging.info(f"Worker started - {view_mode=}") 33 | 34 | page.route("**/*", _hijacker) 35 | page.goto("https://bot.sannysoft.com/", wait_until="networkidle") 36 | 37 | # Save screenshot 38 | _now = datetime.now().strftime("%Y-%m-%d") 39 | if view_mode: 40 | _suffix = f"-view-{view_mode}" 41 | else: 42 | _suffix = f"-view-{datetime.now().strftime('%H%M%S')}" 43 | page.screenshot(path=f"result/{_now}/sannysoft{_suffix}.png", full_page=True) 44 | 45 | logging.info(f"Worker finished - {view_mode=}") 46 | 47 | 48 | def bytedance(view_mode: ViewMode): 49 | with sync_playwright() as p: 50 | match view_mode: 51 | case "new": 52 | args = ["--headless=new", "--dump-dom"] 53 | browser = p.chromium.launch(args=args) 54 | case "headless": 55 | browser = p.chromium.launch(headless=True) 56 | case _: 57 | browser = p.chromium.launch(headless=False) 58 | 59 | context = browser.new_context(locale="en-US") 60 | Tarnished.apply_stealth(context) 61 | 62 | page = context.new_page() 63 | worker(page, view_mode) 64 | 65 | browser.close() 66 | 67 | 68 | def main(): 69 | bytedance(ViewMode.new) 70 | bytedance(ViewMode.headful) 71 | bytedance(ViewMode.headless) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # https://python-poetry.org/docs/libraries/#versioning 2 | 3 | [tool.poetry] 4 | name = "undetected-playwright" 5 | version = "0.3.0" 6 | description = "You know who I am" 7 | license = "Apache-2.0" 8 | authors = ["QIN2DIM "] 9 | readme = "README.md" 10 | homepage = "https://github.com/QIN2DIM/undetected-playwright" 11 | repository = "https://github.com/QIN2DIM/undetected-playwright" 12 | documentation = "https://github.com/QIN2DIM/undetected-playwright" 13 | keywords = ["undetected", "playwright", "steath"] 14 | packages = [{ include = "undetected_playwright", format = "sdist" }] 15 | classifiers = [ 16 | "Topic :: Scientific/Engineering", 17 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 18 | "Topic :: Scientific/Engineering :: Image Processing", 19 | "Topic :: Scientific/Engineering :: Image Recognition", 20 | "Topic :: Software Development", 21 | "Topic :: Software Development :: Libraries", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | "Programming Language :: Python :: 3", 24 | ] 25 | 26 | # https://python-poetry.org/docs/configuration/#virtualenvsin-project 27 | [virtualenvs] 28 | # Create a `.venv` virtual environment in the project root 29 | in-project = true 30 | 31 | # https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups 32 | [tool.poetry.dependencies] 33 | python = "^3.8" 34 | playwright = "*" 35 | 36 | [build-system] 37 | requires = ["poetry-core"] 38 | build-backend = "poetry.core.masonry.api" 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | playwright -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import setup, find_packages 4 | 5 | import undetected_playwright 6 | 7 | # python setup.py sdist bdist_wheel && python -m twine upload dist/* 8 | setup( 9 | name="undetected-playwright", 10 | version=undetected_playwright.__version__, 11 | keywords=["playwright", "undetected-playwright", "playwright-stealth"], 12 | packages=find_packages( 13 | include=["undetected_playwright", "'undetected_playwright'.*", "LICENSE"] 14 | ), 15 | package_data={"undetected_playwright": ["js/*.js"]}, 16 | url="https://github.com/QIN2DIM/undetected-playwright", 17 | license="Apache-2.0 license", 18 | author="QIN2DIM", 19 | author_email="yaoqinse@gmail.com", 20 | description="You know who I am", 21 | long_description=Path(__file__).parent.joinpath("README.md").read_text(), 22 | long_description_content_type="text/markdown", 23 | install_requires=["playwright"], 24 | python_requires=">=3.8", 25 | classifiers=[ 26 | "Topic :: Scientific/Engineering", 27 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 28 | "Topic :: Software Development", 29 | "Topic :: Software Development :: Libraries", 30 | "Topic :: Software Development :: Libraries :: Python Modules", 31 | "License :: OSI Approved :: Apache Software License", 32 | "Programming Language :: Python :: 3", 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /undetected_playwright/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2022/10/22 23:35 3 | # Author : QIN2DIM 4 | # Github : https://github.com/QIN2DIM 5 | # Description: 6 | from .tarnished import Malenia, Tarnished 7 | from .ninja import stealth_async, stealth_sync 8 | 9 | __version__ = "0.3.0" 10 | __all__ = ["stealth_async", "stealth_sync", "Malenia", "Tarnished"] 11 | -------------------------------------------------------------------------------- /undetected_playwright/ninja.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2022/10/24 13:06 3 | # Author : QIN2DIM 4 | # Github : https://github.com/QIN2DIM 5 | # Description: 6 | from pathlib import Path 7 | 8 | from playwright.async_api import BrowserContext as ASyncContext 9 | from playwright.sync_api import BrowserContext as SyncContext 10 | 11 | enabled_evasions = [ 12 | "chrome.app", 13 | "chrome.csi", 14 | "chrome.loadTimes", 15 | "chrome.runtime", 16 | "iframe.contentWindow", 17 | "media.codecs", 18 | "navigator.hardwareConcurrency", 19 | "navigator.languages", 20 | "navigator.permissions", 21 | "navigator.plugins", 22 | "navigator.webdriver", 23 | "sourceurl", 24 | "webgl.vendor", 25 | "window.outerdimensions", 26 | ] 27 | 28 | 29 | def stealth_sync(context: SyncContext, **kwargs) -> SyncContext: 30 | for e in enabled_evasions: 31 | evasion_code = ( 32 | Path(__file__) 33 | .parent.joinpath(f"puppeteer-extra-plugin-stealth/evasions/{e}/index.js") 34 | .read_text(encoding="utf8") 35 | ) 36 | context.add_init_script(evasion_code) 37 | 38 | return context 39 | 40 | 41 | async def stealth_async(context: ASyncContext, **kwargs) -> ASyncContext: 42 | for e in enabled_evasions: 43 | evasion_code = ( 44 | Path(__file__) 45 | .parent.joinpath(f"puppeteer-extra-plugin-stealth/evasions/{e}/index.js") 46 | .read_text(encoding="utf8") 47 | ) 48 | await context.add_init_script(evasion_code) 49 | return context 50 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 berstend 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/_template/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | /** 6 | * Minimal stealth plugin template, not being used. :-) 7 | * 8 | * Feel free to copy this folder as the basis for additional detection evasion plugins. 9 | */ 10 | class Plugin extends PuppeteerExtraPlugin { 11 | constructor(opts = {}) { 12 | super(opts) 13 | } 14 | 15 | get name() { 16 | return 'stealth/evasions/_template' 17 | } 18 | 19 | async onPageCreated(page) { 20 | await page.evaluateOnNewDocument(() => { 21 | console.debug('hello world') 22 | }) 23 | } 24 | } 25 | 26 | module.exports = function(pluginConfig) { 27 | return new Plugin(pluginConfig) 28 | } 29 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/_template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/_utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A set of shared utility functions specifically for the purpose of modifying native browser APIs without leaving traces. 3 | * 4 | * Meant to be passed down in puppeteer and used in the context of the page (everything in here runs in NodeJS as well as a browser). 5 | * 6 | * Note: If for whatever reason you need to use this outside of `puppeteer-extra`: 7 | * Just remove the `module.exports` statement at the very bottom, the rest can be copy pasted into any browser context. 8 | * 9 | * Alternatively take a look at the `extract-stealth-evasions` package to create a finished bundle which includes these utilities. 10 | * 11 | */ 12 | const utils = {} 13 | 14 | utils.init = () => { 15 | utils.preloadCache() 16 | } 17 | 18 | /** 19 | * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw. 20 | * 21 | * The presence of a JS Proxy can be revealed as it shows up in error stack traces. 22 | * 23 | * @param {object} handler - The JS Proxy handler to wrap 24 | */ 25 | utils.stripProxyFromErrors = (handler = {}) => { 26 | const newHandler = { 27 | setPrototypeOf: function (target, proto) { 28 | if (proto === null) 29 | throw new TypeError('Cannot convert object to primitive value') 30 | if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) { 31 | throw new TypeError('Cyclic __proto__ value') 32 | } 33 | return Reflect.setPrototypeOf(target, proto) 34 | } 35 | } 36 | // We wrap each trap in the handler in a try/catch and modify the error stack if they throw 37 | const traps = Object.getOwnPropertyNames(handler) 38 | traps.forEach(trap => { 39 | newHandler[trap] = function () { 40 | try { 41 | // Forward the call to the defined proxy handler 42 | return handler[trap].apply(this, arguments || []) 43 | } catch (err) { 44 | // Stack traces differ per browser, we only support chromium based ones currently 45 | if (!err || !err.stack || !err.stack.includes(`at `)) { 46 | throw err 47 | } 48 | 49 | // When something throws within one of our traps the Proxy will show up in error stacks 50 | // An earlier implementation of this code would simply strip lines with a blacklist, 51 | // but it makes sense to be more surgical here and only remove lines related to our Proxy. 52 | // We try to use a known "anchor" line for that and strip it with everything above it. 53 | // If the anchor line cannot be found for some reason we fall back to our blacklist approach. 54 | 55 | const stripWithBlacklist = (stack, stripFirstLine = true) => { 56 | const blacklist = [ 57 | `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply 58 | `at Object.${trap} `, // e.g. Object.get or Object.apply 59 | `at Object.newHandler. [as ${trap}] ` // caused by this very wrapper :-) 60 | ] 61 | return ( 62 | err.stack 63 | .split('\n') 64 | // Always remove the first (file) line in the stack (guaranteed to be our proxy) 65 | .filter((line, index) => !(index === 1 && stripFirstLine)) 66 | // Check if the line starts with one of our blacklisted strings 67 | .filter(line => !blacklist.some(bl => line.trim().startsWith(bl))) 68 | .join('\n') 69 | ) 70 | } 71 | 72 | const stripWithAnchor = (stack, anchor) => { 73 | const stackArr = stack.split('\n') 74 | anchor = anchor || `at Object.newHandler. [as ${trap}] ` // Known first Proxy line in chromium 75 | const anchorIndex = stackArr.findIndex(line => 76 | line.trim().startsWith(anchor) 77 | ) 78 | if (anchorIndex === -1) { 79 | return false // 404, anchor not found 80 | } 81 | // Strip everything from the top until we reach the anchor line 82 | // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`) 83 | stackArr.splice(1, anchorIndex) 84 | return stackArr.join('\n') 85 | } 86 | 87 | // Special cases due to our nested toString proxies 88 | err.stack = err.stack.replace( 89 | 'at Object.toString (', 90 | 'at Function.toString (' 91 | ) 92 | if ((err.stack || '').includes('at Function.toString (')) { 93 | err.stack = stripWithBlacklist(err.stack, false) 94 | throw err 95 | } 96 | 97 | // Try using the anchor method, fallback to blacklist if necessary 98 | err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack) 99 | 100 | throw err // Re-throw our now sanitized error 101 | } 102 | } 103 | }) 104 | return newHandler 105 | } 106 | 107 | /** 108 | * Strip error lines from stack traces until (and including) a known line the stack. 109 | * 110 | * @param {object} err - The error to sanitize 111 | * @param {string} anchor - The string the anchor line starts with 112 | */ 113 | utils.stripErrorWithAnchor = (err, anchor) => { 114 | const stackArr = err.stack.split('\n') 115 | const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor)) 116 | if (anchorIndex === -1) { 117 | return err // 404, anchor not found 118 | } 119 | // Strip everything from the top until we reach the anchor line (remove anchor line as well) 120 | // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`) 121 | stackArr.splice(1, anchorIndex) 122 | err.stack = stackArr.join('\n') 123 | return err 124 | } 125 | 126 | /** 127 | * Replace the property of an object in a stealthy way. 128 | * 129 | * Note: You also want to work on the prototype of an object most often, 130 | * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)). 131 | * 132 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 133 | * 134 | * @example 135 | * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" }) 136 | * // or 137 | * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] }) 138 | * 139 | * @param {object} obj - The object which has the property to replace 140 | * @param {string} propName - The property name to replace 141 | * @param {object} descriptorOverrides - e.g. { value: "alice" } 142 | */ 143 | utils.replaceProperty = (obj, propName, descriptorOverrides = {}) => { 144 | return Object.defineProperty(obj, propName, { 145 | // Copy over the existing descriptors (writable, enumerable, configurable, etc) 146 | ...(Object.getOwnPropertyDescriptor(obj, propName) || {}), 147 | // Add our overrides (e.g. value, get()) 148 | ...descriptorOverrides 149 | }) 150 | } 151 | 152 | /** 153 | * Preload a cache of function copies and data. 154 | * 155 | * For a determined enough observer it would be possible to overwrite and sniff usage of functions 156 | * we use in our internal Proxies, to combat that we use a cached copy of those functions. 157 | * 158 | * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before, 159 | * by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups). 160 | * 161 | * This is evaluated once per execution context (e.g. window) 162 | */ 163 | utils.preloadCache = () => { 164 | if (utils.cache) { 165 | return 166 | } 167 | utils.cache = { 168 | // Used in our proxies 169 | Reflect: { 170 | get: Reflect.get.bind(Reflect), 171 | apply: Reflect.apply.bind(Reflect) 172 | }, 173 | // Used in `makeNativeString` 174 | nativeToStringStr: Function.toString + '' // => `function toString() { [native code] }` 175 | } 176 | } 177 | 178 | /** 179 | * Utility function to generate a cross-browser `toString` result representing native code. 180 | * 181 | * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings. 182 | * To future-proof this we use an existing native toString result as the basis. 183 | * 184 | * The only advantage we have over the other team is that our JS runs first, hence we cache the result 185 | * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it. 186 | * 187 | * @example 188 | * makeNativeString('foobar') // => `function foobar() { [native code] }` 189 | * 190 | * @param {string} [name] - Optional function name 191 | */ 192 | utils.makeNativeString = (name = '') => { 193 | return utils.cache.nativeToStringStr.replace('toString', name || '') 194 | } 195 | 196 | /** 197 | * Helper function to modify the `toString()` result of the provided object. 198 | * 199 | * Note: Use `utils.redirectToString` instead when possible. 200 | * 201 | * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object. 202 | * If no string is provided we will generate a `[native code]` thing based on the name of the property object. 203 | * 204 | * @example 205 | * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }') 206 | * 207 | * @param {object} obj - The object for which to modify the `toString()` representation 208 | * @param {string} str - Optional string used as a return value 209 | */ 210 | utils.patchToString = (obj, str = '') => { 211 | const handler = { 212 | apply: function (target, ctx) { 213 | // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` 214 | if (ctx === Function.prototype.toString) { 215 | return utils.makeNativeString('toString') 216 | } 217 | // `toString` targeted at our proxied Object detected 218 | if (ctx === obj) { 219 | // We either return the optional string verbatim or derive the most desired result automatically 220 | return str || utils.makeNativeString(obj.name) 221 | } 222 | // Check if the toString protype of the context is the same as the global prototype, 223 | // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case 224 | const hasSameProto = Object.getPrototypeOf( 225 | Function.prototype.toString 226 | ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins 227 | if (!hasSameProto) { 228 | // Pass the call on to the local Function.prototype.toString instead 229 | return ctx.toString() 230 | } 231 | return target.call(ctx) 232 | } 233 | } 234 | 235 | const toStringProxy = new Proxy( 236 | Function.prototype.toString, 237 | utils.stripProxyFromErrors(handler) 238 | ) 239 | utils.replaceProperty(Function.prototype, 'toString', { 240 | value: toStringProxy 241 | }) 242 | } 243 | 244 | /** 245 | * Make all nested functions of an object native. 246 | * 247 | * @param {object} obj 248 | */ 249 | utils.patchToStringNested = (obj = {}) => { 250 | return utils.execRecursively(obj, ['function'], utils.patchToString) 251 | } 252 | 253 | /** 254 | * Redirect toString requests from one object to another. 255 | * 256 | * @param {object} proxyObj - The object that toString will be called on 257 | * @param {object} originalObj - The object which toString result we wan to return 258 | */ 259 | utils.redirectToString = (proxyObj, originalObj) => { 260 | const handler = { 261 | apply: function (target, ctx) { 262 | // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` 263 | if (ctx === Function.prototype.toString) { 264 | return utils.makeNativeString('toString') 265 | } 266 | 267 | // `toString` targeted at our proxied Object detected 268 | if (ctx === proxyObj) { 269 | const fallback = () => 270 | originalObj && originalObj.name 271 | ? utils.makeNativeString(originalObj.name) 272 | : utils.makeNativeString(proxyObj.name) 273 | 274 | // Return the toString representation of our original object if possible 275 | return originalObj + '' || fallback() 276 | } 277 | 278 | if (typeof ctx === 'undefined' || ctx === null) { 279 | return target.call(ctx) 280 | } 281 | 282 | // Check if the toString protype of the context is the same as the global prototype, 283 | // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case 284 | const hasSameProto = Object.getPrototypeOf( 285 | Function.prototype.toString 286 | ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins 287 | if (!hasSameProto) { 288 | // Pass the call on to the local Function.prototype.toString instead 289 | return ctx.toString() 290 | } 291 | 292 | return target.call(ctx) 293 | } 294 | } 295 | 296 | const toStringProxy = new Proxy( 297 | Function.prototype.toString, 298 | utils.stripProxyFromErrors(handler) 299 | ) 300 | utils.replaceProperty(Function.prototype, 'toString', { 301 | value: toStringProxy 302 | }) 303 | } 304 | 305 | /** 306 | * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps. 307 | * 308 | * Will stealthify these aspects (strip error stack traces, redirect toString, etc). 309 | * Note: This is meant to modify native Browser APIs and works best with prototype objects. 310 | * 311 | * @example 312 | * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler) 313 | * 314 | * @param {object} obj - The object which has the property to replace 315 | * @param {string} propName - The name of the property to replace 316 | * @param {object} handler - The JS Proxy handler to use 317 | */ 318 | utils.replaceWithProxy = (obj, propName, handler) => { 319 | const originalObj = obj[propName] 320 | const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler)) 321 | 322 | utils.replaceProperty(obj, propName, { value: proxyObj }) 323 | utils.redirectToString(proxyObj, originalObj) 324 | 325 | return true 326 | } 327 | /** 328 | * All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps. 329 | * 330 | * @example 331 | * replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler) 332 | * 333 | * @param {object} obj - The object which has the property to replace 334 | * @param {string} propName - The name of the property to replace 335 | * @param {object} handler - The JS Proxy handler to use 336 | */ 337 | utils.replaceGetterWithProxy = (obj, propName, handler) => { 338 | const fn = Object.getOwnPropertyDescriptor(obj, propName).get 339 | const fnStr = fn.toString() // special getter function string 340 | const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler)) 341 | 342 | utils.replaceProperty(obj, propName, { get: proxyObj }) 343 | utils.patchToString(proxyObj, fnStr) 344 | 345 | return true 346 | } 347 | 348 | /** 349 | * All-in-one method to replace a getter and/or setter. Functions get and set 350 | * of handler have one more argument that contains the native function. 351 | * 352 | * @example 353 | * replaceGetterSetter(HTMLIFrameElement.prototype, 'contentWindow', handler) 354 | * 355 | * @param {object} obj - The object which has the property to replace 356 | * @param {string} propName - The name of the property to replace 357 | * @param {object} handlerGetterSetter - The handler with get and/or set 358 | * functions 359 | * @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description 360 | */ 361 | utils.replaceGetterSetter = (obj, propName, handlerGetterSetter) => { 362 | const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName) 363 | const handler = { ...ownPropertyDescriptor } 364 | 365 | if (handlerGetterSetter.get !== undefined) { 366 | const nativeFn = ownPropertyDescriptor.get 367 | handler.get = function() { 368 | return handlerGetterSetter.get.call(this, nativeFn.bind(this)) 369 | } 370 | utils.redirectToString(handler.get, nativeFn) 371 | } 372 | 373 | if (handlerGetterSetter.set !== undefined) { 374 | const nativeFn = ownPropertyDescriptor.set 375 | handler.set = function(newValue) { 376 | handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this)) 377 | } 378 | utils.redirectToString(handler.set, nativeFn) 379 | } 380 | 381 | Object.defineProperty(obj, propName, handler) 382 | } 383 | 384 | /** 385 | * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps. 386 | * 387 | * Will stealthify these aspects (strip error stack traces, redirect toString, etc). 388 | * 389 | * @example 390 | * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler) 391 | * 392 | * @param {object} obj - The object which has the property to replace 393 | * @param {string} propName - The name of the property to replace or create 394 | * @param {object} pseudoTarget - The JS Proxy target to use as a basis 395 | * @param {object} handler - The JS Proxy handler to use 396 | */ 397 | utils.mockWithProxy = (obj, propName, pseudoTarget, handler) => { 398 | const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler)) 399 | 400 | utils.replaceProperty(obj, propName, { value: proxyObj }) 401 | utils.patchToString(proxyObj) 402 | 403 | return true 404 | } 405 | 406 | /** 407 | * All-in-one method to create a new JS Proxy with stealth tweaks. 408 | * 409 | * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property. 410 | * 411 | * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc). 412 | * 413 | * @example 414 | * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy 415 | * 416 | * @param {object} pseudoTarget - The JS Proxy target to use as a basis 417 | * @param {object} handler - The JS Proxy handler to use 418 | */ 419 | utils.createProxy = (pseudoTarget, handler) => { 420 | const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler)) 421 | utils.patchToString(proxyObj) 422 | 423 | return proxyObj 424 | } 425 | 426 | /** 427 | * Helper function to split a full path to an Object into the first part and property. 428 | * 429 | * @example 430 | * splitObjPath(`HTMLMediaElement.prototype.canPlayType`) 431 | * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"} 432 | * 433 | * @param {string} objPath - The full path to an object as dot notation string 434 | */ 435 | utils.splitObjPath = objPath => ({ 436 | // Remove last dot entry (property) ==> `HTMLMediaElement.prototype` 437 | objName: objPath.split('.').slice(0, -1).join('.'), 438 | // Extract last dot entry ==> `canPlayType` 439 | propName: objPath.split('.').slice(-1)[0] 440 | }) 441 | 442 | /** 443 | * Convenience method to replace a property with a JS Proxy using the provided objPath. 444 | * 445 | * Supports a full path (dot notation) to the object as string here, in case that makes it easier. 446 | * 447 | * @example 448 | * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler) 449 | * 450 | * @param {string} objPath - The full path to an object (dot notation string) to replace 451 | * @param {object} handler - The JS Proxy handler to use 452 | */ 453 | utils.replaceObjPathWithProxy = (objPath, handler) => { 454 | const { objName, propName } = utils.splitObjPath(objPath) 455 | const obj = eval(objName) // eslint-disable-line no-eval 456 | return utils.replaceWithProxy(obj, propName, handler) 457 | } 458 | 459 | /** 460 | * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types. 461 | * 462 | * @param {object} obj 463 | * @param {array} typeFilter - e.g. `['function']` 464 | * @param {Function} fn - e.g. `utils.patchToString` 465 | */ 466 | utils.execRecursively = (obj = {}, typeFilter = [], fn) => { 467 | function recurse(obj) { 468 | for (const key in obj) { 469 | if (obj[key] === undefined) { 470 | continue 471 | } 472 | if (obj[key] && typeof obj[key] === 'object') { 473 | recurse(obj[key]) 474 | } else { 475 | if (obj[key] && typeFilter.includes(typeof obj[key])) { 476 | fn.call(this, obj[key]) 477 | } 478 | } 479 | } 480 | } 481 | recurse(obj) 482 | return obj 483 | } 484 | 485 | /** 486 | * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one. 487 | * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter. 488 | * 489 | * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process. 490 | * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings. 491 | * 492 | * We use this to pass down our utility functions as well as any other functions (to be able to split up code better). 493 | * 494 | * @see utils.materializeFns 495 | * 496 | * @param {object} fnObj - An object containing functions as properties 497 | */ 498 | utils.stringifyFns = (fnObj = { hello: () => 'world' }) => { 499 | // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine 500 | // https://github.com/feross/fromentries 501 | function fromEntries(iterable) { 502 | return [...iterable].reduce((obj, [key, val]) => { 503 | obj[key] = val 504 | return obj 505 | }, {}) 506 | } 507 | return (Object.fromEntries || fromEntries)( 508 | Object.entries(fnObj) 509 | .filter(([key, value]) => typeof value === 'function') 510 | .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval 511 | ) 512 | } 513 | 514 | /** 515 | * Utility function to reverse the process of `utils.stringifyFns`. 516 | * Will materialize an object with stringified functions (supports classic and fat arrow functions). 517 | * 518 | * @param {object} fnStrObj - An object containing stringified functions as properties 519 | */ 520 | utils.materializeFns = (fnStrObj = { hello: "() => 'world'" }) => { 521 | return Object.fromEntries( 522 | Object.entries(fnStrObj).map(([key, value]) => { 523 | if (value.startsWith('function')) { 524 | // some trickery is needed to make oldschool functions work :-) 525 | return [key, eval(`() => ${value}`)()] // eslint-disable-line no-eval 526 | } else { 527 | // arrow functions just work 528 | return [key, eval(value)] // eslint-disable-line no-eval 529 | } 530 | }) 531 | ) 532 | } 533 | 534 | // Proxy handler templates for re-usability 535 | utils.makeHandler = () => ({ 536 | // Used by simple `navigator` getter evasions 537 | getterValue: value => ({ 538 | apply(target, ctx, args) { 539 | // Let's fetch the value first, to trigger and escalate potential errors 540 | // Illegal invocations like `navigator.__proto__.vendor` will throw here 541 | utils.cache.Reflect.apply(...arguments) 542 | return value 543 | } 544 | }) 545 | }) 546 | 547 | /** 548 | * Compare two arrays. 549 | * 550 | * @param {array} array1 - First array 551 | * @param {array} array2 - Second array 552 | */ 553 | utils.arrayEquals = (array1, array2) => { 554 | if (array1.length !== array2.length) { 555 | return false 556 | } 557 | for (let i = 0; i < array1.length; ++i) { 558 | if (array1[i] !== array2[i]) { 559 | return false 560 | } 561 | } 562 | return true 563 | } 564 | 565 | /** 566 | * Cache the method return according to its arguments. 567 | * 568 | * @param {Function} fn - A function that will be cached 569 | */ 570 | utils.memoize = fn => { 571 | const cache = [] 572 | return function(...args) { 573 | if (!cache.some(c => utils.arrayEquals(c.key, args))) { 574 | cache.push({ key: args, value: fn.apply(this, args) }) 575 | } 576 | return cache.find(c => utils.arrayEquals(c.key, args)).value 577 | } 578 | } 579 | 580 | // -- 581 | // Stuff starting below this line is NodeJS specific. 582 | // -- 583 | module.exports = utils 584 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/_utils/withUtils.js: -------------------------------------------------------------------------------- 1 | const utils = require('./index') 2 | 3 | /** 4 | * Wrap a page with utilities. 5 | * 6 | * @param {Puppeteer.Page} page 7 | */ 8 | module.exports = page => ({ 9 | /** 10 | * Simple `page.evaluate` replacement to preload utils 11 | */ 12 | evaluate: async function (mainFunction, ...args) { 13 | return page.evaluate( 14 | ({ _utilsFns, _mainFunction, _args }) => { 15 | // Add this point we cannot use our utililty functions as they're just strings, we need to materialize them first 16 | const utils = Object.fromEntries( 17 | Object.entries(_utilsFns).map(([key, value]) => [key, eval(value)]) // eslint-disable-line no-eval 18 | ) 19 | utils.init() 20 | return eval(_mainFunction)(utils, ..._args) // eslint-disable-line no-eval 21 | }, 22 | { 23 | _utilsFns: utils.stringifyFns(utils), 24 | _mainFunction: mainFunction.toString(), 25 | _args: args || [] 26 | } 27 | ) 28 | }, 29 | /** 30 | * Simple `page.evaluateOnNewDocument` replacement to preload utils 31 | */ 32 | evaluateOnNewDocument: async function (mainFunction, ...args) { 33 | return page.evaluateOnNewDocument( 34 | ({ _utilsFns, _mainFunction, _args }) => { 35 | // Add this point we cannot use our utililty functions as they're just strings, we need to materialize them first 36 | const utils = Object.fromEntries( 37 | Object.entries(_utilsFns).map(([key, value]) => [key, eval(value)]) // eslint-disable-line no-eval 38 | ) 39 | utils.init() 40 | return eval(_mainFunction)(utils, ..._args) // eslint-disable-line no-eval 41 | }, 42 | { 43 | _utilsFns: utils.stringifyFns(utils), 44 | _mainFunction: mainFunction.toString(), 45 | _args: args || [] 46 | } 47 | ) 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Mock the `chrome.app` object if not available (e.g. when running headless). 9 | */ 10 | class Plugin extends PuppeteerExtraPlugin { 11 | constructor(opts = {}) { 12 | super(opts) 13 | } 14 | 15 | get name() { 16 | return 'stealth/evasions/chrome.app' 17 | } 18 | 19 | async onPageCreated(page) { 20 | await withUtils(page).evaluateOnNewDocument(utils => { 21 | if (!window.chrome) { 22 | // Use the exact property descriptor found in headful Chrome 23 | // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')` 24 | Object.defineProperty(window, 'chrome', { 25 | writable: true, 26 | enumerable: true, 27 | configurable: false, // note! 28 | value: {} // We'll extend that later 29 | }) 30 | } 31 | 32 | // That means we're running headful and don't need to mock anything 33 | if ('app' in window.chrome) { 34 | return // Nothing to do here 35 | } 36 | 37 | const makeError = { 38 | ErrorInInvocation: fn => { 39 | const err = new TypeError(`Error in invocation of app.${fn}()`) 40 | return utils.stripErrorWithAnchor( 41 | err, 42 | `at ${fn} (eval at ` 43 | ) 44 | } 45 | } 46 | 47 | // There's a some static data in that property which doesn't seem to change, 48 | // we should periodically check for updates: `JSON.stringify(window.app, null, 2)` 49 | const STATIC_DATA = JSON.parse( 50 | ` 51 | { 52 | "isInstalled": false, 53 | "InstallState": { 54 | "DISABLED": "disabled", 55 | "INSTALLED": "installed", 56 | "NOT_INSTALLED": "not_installed" 57 | }, 58 | "RunningState": { 59 | "CANNOT_RUN": "cannot_run", 60 | "READY_TO_RUN": "ready_to_run", 61 | "RUNNING": "running" 62 | } 63 | } 64 | `.trim() 65 | ) 66 | 67 | window.chrome.app = { 68 | ...STATIC_DATA, 69 | 70 | get isInstalled() { 71 | return false 72 | }, 73 | 74 | getDetails: function getDetails() { 75 | if (arguments.length) { 76 | throw makeError.ErrorInInvocation(`getDetails`) 77 | } 78 | return null 79 | }, 80 | getIsInstalled: function getDetails() { 81 | if (arguments.length) { 82 | throw makeError.ErrorInInvocation(`getIsInstalled`) 83 | } 84 | return false 85 | }, 86 | runningState: function getDetails() { 87 | if (arguments.length) { 88 | throw makeError.ErrorInInvocation(`runningState`) 89 | } 90 | return 'cannot_run' 91 | } 92 | } 93 | utils.patchToStringNested(window.chrome.app) 94 | }) 95 | } 96 | } 97 | 98 | module.exports = function(pluginConfig) { 99 | return new Plugin(pluginConfig) 100 | } 101 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.csi/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Mock the `chrome.csi` function if not available (e.g. when running headless). 9 | * It's a deprecated (but unfortunately still existing) chrome specific API to fetch browser timings. 10 | * 11 | * Internally chromium switched the implementation to use the WebPerformance API, 12 | * so we can do the same to create a fully functional mock. :-) 13 | * 14 | * Note: We're using the deprecated PerformanceTiming API instead of the new Navigation Timing Level 2 API on purpopse. 15 | * 16 | * @see https://bugs.chromium.org/p/chromium/issues/detail?id=113048 17 | * @see https://codereview.chromium.org/2456293003/ 18 | * @see https://developers.google.com/web/updates/2017/12/chrome-loadtimes-deprecated 19 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming 20 | * @see https://source.chromium.org/chromium/chromium/src/+/master:chrome/renderer/loadtimes_extension_bindings.cc;l=124?q=loadtimes&ss=chromium 21 | * @see `chrome.loadTimes` evasion 22 | * 23 | */ 24 | class Plugin extends PuppeteerExtraPlugin { 25 | constructor(opts = {}) { 26 | super(opts) 27 | } 28 | 29 | get name() { 30 | return 'stealth/evasions/chrome.csi' 31 | } 32 | 33 | async onPageCreated(page) { 34 | await withUtils(page).evaluateOnNewDocument(utils => { 35 | if (!window.chrome) { 36 | // Use the exact property descriptor found in headful Chrome 37 | // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')` 38 | Object.defineProperty(window, 'chrome', { 39 | writable: true, 40 | enumerable: true, 41 | configurable: false, // note! 42 | value: {} // We'll extend that later 43 | }) 44 | } 45 | 46 | // That means we're running headful and don't need to mock anything 47 | if ('csi' in window.chrome) { 48 | return // Nothing to do here 49 | } 50 | 51 | // Check that the Navigation Timing API v1 is available, we need that 52 | if (!window.performance || !window.performance.timing) { 53 | return 54 | } 55 | 56 | const { timing } = window.performance 57 | 58 | window.chrome.csi = function() { 59 | return { 60 | onloadT: timing.domContentLoadedEventEnd, 61 | startE: timing.navigationStart, 62 | pageT: Date.now() - timing.navigationStart, 63 | tran: 15 // Transition type or something 64 | } 65 | } 66 | utils.patchToString(window.chrome.csi) 67 | }) 68 | } 69 | } 70 | 71 | module.exports = function(pluginConfig) { 72 | return new Plugin(pluginConfig) 73 | } 74 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.csi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Mock the `chrome.loadTimes` function if not available (e.g. when running headless). 9 | * It's a deprecated (but unfortunately still existing) chrome specific API to fetch browser timings and connection info. 10 | * 11 | * Internally chromium switched the implementation to use the WebPerformance API, 12 | * so we can do the same to create a fully functional mock. :-) 13 | * 14 | * Note: We're using the deprecated PerformanceTiming API instead of the new Navigation Timing Level 2 API on purpopse. 15 | * 16 | * @see https://developers.google.com/web/updates/2017/12/chrome-loadtimes-deprecated 17 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming 18 | * @see https://source.chromium.org/chromium/chromium/src/+/master:chrome/renderer/loadtimes_extension_bindings.cc;l=124?q=loadtimes&ss=chromium 19 | * @see `chrome.csi` evasion 20 | * 21 | */ 22 | class Plugin extends PuppeteerExtraPlugin { 23 | constructor(opts = {}) { 24 | super(opts) 25 | } 26 | 27 | get name() { 28 | return 'stealth/evasions/chrome.loadTimes' 29 | } 30 | 31 | async onPageCreated(page) { 32 | await withUtils(page).evaluateOnNewDocument( 33 | (utils, { opts }) => { 34 | if (!window.chrome) { 35 | // Use the exact property descriptor found in headful Chrome 36 | // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')` 37 | Object.defineProperty(window, 'chrome', { 38 | writable: true, 39 | enumerable: true, 40 | configurable: false, // note! 41 | value: {} // We'll extend that later 42 | }) 43 | } 44 | 45 | // That means we're running headful and don't need to mock anything 46 | if ('loadTimes' in window.chrome) { 47 | return // Nothing to do here 48 | } 49 | 50 | // Check that the Navigation Timing API v1 + v2 is available, we need that 51 | if ( 52 | !window.performance || 53 | !window.performance.timing || 54 | !window.PerformancePaintTiming 55 | ) { 56 | return 57 | } 58 | 59 | const { performance } = window 60 | 61 | // Some stuff is not available on about:blank as it requires a navigation to occur, 62 | // let's harden the code to not fail then: 63 | const ntEntryFallback = { 64 | nextHopProtocol: 'h2', 65 | type: 'other' 66 | } 67 | 68 | // The API exposes some funky info regarding the connection 69 | const protocolInfo = { 70 | get connectionInfo() { 71 | const ntEntry = 72 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 73 | return ntEntry.nextHopProtocol 74 | }, 75 | get npnNegotiatedProtocol() { 76 | // NPN is deprecated in favor of ALPN, but this implementation returns the 77 | // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN. 78 | const ntEntry = 79 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 80 | return ['h2', 'hq'].includes(ntEntry.nextHopProtocol) 81 | ? ntEntry.nextHopProtocol 82 | : 'unknown' 83 | }, 84 | get navigationType() { 85 | const ntEntry = 86 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 87 | return ntEntry.type 88 | }, 89 | get wasAlternateProtocolAvailable() { 90 | // The Alternate-Protocol header is deprecated in favor of Alt-Svc 91 | // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this 92 | // should always return false. 93 | return false 94 | }, 95 | get wasFetchedViaSpdy() { 96 | // SPDY is deprecated in favor of HTTP/2, but this implementation returns 97 | // true for HTTP/2 or HTTP2+QUIC/39 as well. 98 | const ntEntry = 99 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 100 | return ['h2', 'hq'].includes(ntEntry.nextHopProtocol) 101 | }, 102 | get wasNpnNegotiated() { 103 | // NPN is deprecated in favor of ALPN, but this implementation returns true 104 | // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN. 105 | const ntEntry = 106 | performance.getEntriesByType('navigation')[0] || ntEntryFallback 107 | return ['h2', 'hq'].includes(ntEntry.nextHopProtocol) 108 | } 109 | } 110 | 111 | const { timing } = window.performance 112 | 113 | // Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3 114 | function toFixed(num, fixed) { 115 | var re = new RegExp('^-?\\d+(?:.\\d{0,' + (fixed || -1) + '})?') 116 | return num.toString().match(re)[0] 117 | } 118 | 119 | const timingInfo = { 120 | get firstPaintAfterLoadTime() { 121 | // This was never actually implemented and always returns 0. 122 | return 0 123 | }, 124 | get requestTime() { 125 | return timing.navigationStart / 1000 126 | }, 127 | get startLoadTime() { 128 | return timing.navigationStart / 1000 129 | }, 130 | get commitLoadTime() { 131 | return timing.responseStart / 1000 132 | }, 133 | get finishDocumentLoadTime() { 134 | return timing.domContentLoadedEventEnd / 1000 135 | }, 136 | get finishLoadTime() { 137 | return timing.loadEventEnd / 1000 138 | }, 139 | get firstPaintTime() { 140 | const fpEntry = performance.getEntriesByType('paint')[0] || { 141 | startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`) 142 | } 143 | return toFixed( 144 | (fpEntry.startTime + performance.timeOrigin) / 1000, 145 | 3 146 | ) 147 | } 148 | } 149 | 150 | window.chrome.loadTimes = function() { 151 | return { 152 | ...protocolInfo, 153 | ...timingInfo 154 | } 155 | } 156 | utils.patchToString(window.chrome.loadTimes) 157 | }, 158 | { 159 | opts: this.opts 160 | } 161 | ) 162 | } 163 | } 164 | 165 | module.exports = function(pluginConfig) { 166 | return new Plugin(pluginConfig) 167 | } 168 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | const STATIC_DATA = require('./staticData.json') 8 | 9 | /** 10 | * Mock the `chrome.runtime` object if not available (e.g. when running headless) and on a secure site. 11 | */ 12 | class Plugin extends PuppeteerExtraPlugin { 13 | constructor(opts = {}) { 14 | super(opts) 15 | } 16 | 17 | get name() { 18 | return 'stealth/evasions/chrome.runtime' 19 | } 20 | 21 | get defaults() { 22 | return { runOnInsecureOrigins: false } // Override for testing 23 | } 24 | 25 | async onPageCreated(page) { 26 | await withUtils(page).evaluateOnNewDocument( 27 | (utils, { opts, STATIC_DATA }) => { 28 | if (!window.chrome) { 29 | // Use the exact property descriptor found in headful Chrome 30 | // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')` 31 | Object.defineProperty(window, 'chrome', { 32 | writable: true, 33 | enumerable: true, 34 | configurable: false, // note! 35 | value: {} // We'll extend that later 36 | }) 37 | } 38 | 39 | // That means we're running headful and don't need to mock anything 40 | const existsAlready = 'runtime' in window.chrome 41 | // `chrome.runtime` is only exposed on secure origins 42 | const isNotSecure = !window.location.protocol.startsWith('https') 43 | if (existsAlready || (isNotSecure && !opts.runOnInsecureOrigins)) { 44 | return // Nothing to do here 45 | } 46 | 47 | window.chrome.runtime = { 48 | // There's a bunch of static data in that property which doesn't seem to change, 49 | // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)` 50 | ...STATIC_DATA, 51 | // `chrome.runtime.id` is extension related and returns undefined in Chrome 52 | get id() { 53 | return undefined 54 | }, 55 | // These two require more sophisticated mocks 56 | connect: null, 57 | sendMessage: null 58 | } 59 | 60 | const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({ 61 | NoMatchingSignature: new TypeError( 62 | preamble + `No matching signature.` 63 | ), 64 | MustSpecifyExtensionID: new TypeError( 65 | preamble + 66 | `${method} called from a webpage must specify an Extension ID (string) for its first argument.` 67 | ), 68 | InvalidExtensionID: new TypeError( 69 | preamble + `Invalid extension id: '${extensionId}'` 70 | ) 71 | }) 72 | 73 | // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`: 74 | // https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90 75 | const isValidExtensionID = str => 76 | str.length === 32 && str.toLowerCase().match(/^[a-p]+$/) 77 | 78 | /** Mock `chrome.runtime.sendMessage` */ 79 | const sendMessageHandler = { 80 | apply: function(target, ctx, args) { 81 | const [extensionId, options, responseCallback] = args || [] 82 | 83 | // Define custom errors 84 | const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): ` 85 | const Errors = makeCustomRuntimeErrors( 86 | errorPreamble, 87 | `chrome.runtime.sendMessage()`, 88 | extensionId 89 | ) 90 | 91 | // Check if the call signature looks ok 92 | const noArguments = args.length === 0 93 | const tooManyArguments = args.length > 4 94 | const incorrectOptions = options && typeof options !== 'object' 95 | const incorrectResponseCallback = 96 | responseCallback && typeof responseCallback !== 'function' 97 | if ( 98 | noArguments || 99 | tooManyArguments || 100 | incorrectOptions || 101 | incorrectResponseCallback 102 | ) { 103 | throw Errors.NoMatchingSignature 104 | } 105 | 106 | // At least 2 arguments are required before we even validate the extension ID 107 | if (args.length < 2) { 108 | throw Errors.MustSpecifyExtensionID 109 | } 110 | 111 | // Now let's make sure we got a string as extension ID 112 | if (typeof extensionId !== 'string') { 113 | throw Errors.NoMatchingSignature 114 | } 115 | 116 | if (!isValidExtensionID(extensionId)) { 117 | throw Errors.InvalidExtensionID 118 | } 119 | 120 | return undefined // Normal behavior 121 | } 122 | } 123 | utils.mockWithProxy( 124 | window.chrome.runtime, 125 | 'sendMessage', 126 | function sendMessage() {}, 127 | sendMessageHandler 128 | ) 129 | 130 | /** 131 | * Mock `chrome.runtime.connect` 132 | * 133 | * @see https://developer.chrome.com/apps/runtime#method-connect 134 | */ 135 | const connectHandler = { 136 | apply: function(target, ctx, args) { 137 | const [extensionId, connectInfo] = args || [] 138 | 139 | // Define custom errors 140 | const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): ` 141 | const Errors = makeCustomRuntimeErrors( 142 | errorPreamble, 143 | `chrome.runtime.connect()`, 144 | extensionId 145 | ) 146 | 147 | // Behavior differs a bit from sendMessage: 148 | const noArguments = args.length === 0 149 | const emptyStringArgument = args.length === 1 && extensionId === '' 150 | if (noArguments || emptyStringArgument) { 151 | throw Errors.MustSpecifyExtensionID 152 | } 153 | 154 | const tooManyArguments = args.length > 2 155 | const incorrectConnectInfoType = 156 | connectInfo && typeof connectInfo !== 'object' 157 | 158 | if (tooManyArguments || incorrectConnectInfoType) { 159 | throw Errors.NoMatchingSignature 160 | } 161 | 162 | const extensionIdIsString = typeof extensionId === 'string' 163 | if (extensionIdIsString && extensionId === '') { 164 | throw Errors.MustSpecifyExtensionID 165 | } 166 | if (extensionIdIsString && !isValidExtensionID(extensionId)) { 167 | throw Errors.InvalidExtensionID 168 | } 169 | 170 | // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate 171 | const validateConnectInfo = ci => { 172 | // More than a first param connectInfo as been provided 173 | if (args.length > 1) { 174 | throw Errors.NoMatchingSignature 175 | } 176 | // An empty connectInfo has been provided 177 | if (Object.keys(ci).length === 0) { 178 | throw Errors.MustSpecifyExtensionID 179 | } 180 | // Loop over all connectInfo props an check them 181 | Object.entries(ci).forEach(([k, v]) => { 182 | const isExpected = ['name', 'includeTlsChannelId'].includes(k) 183 | if (!isExpected) { 184 | throw new TypeError( 185 | errorPreamble + `Unexpected property: '${k}'.` 186 | ) 187 | } 188 | const MismatchError = (propName, expected, found) => 189 | TypeError( 190 | errorPreamble + 191 | `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.` 192 | ) 193 | if (k === 'name' && typeof v !== 'string') { 194 | throw MismatchError(k, 'string', typeof v) 195 | } 196 | if (k === 'includeTlsChannelId' && typeof v !== 'boolean') { 197 | throw MismatchError(k, 'boolean', typeof v) 198 | } 199 | }) 200 | } 201 | if (typeof extensionId === 'object') { 202 | validateConnectInfo(extensionId) 203 | throw Errors.MustSpecifyExtensionID 204 | } 205 | 206 | // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well 207 | return utils.patchToStringNested(makeConnectResponse()) 208 | } 209 | } 210 | utils.mockWithProxy( 211 | window.chrome.runtime, 212 | 'connect', 213 | function connect() {}, 214 | connectHandler 215 | ) 216 | 217 | function makeConnectResponse() { 218 | const onSomething = () => ({ 219 | addListener: function addListener() {}, 220 | dispatch: function dispatch() {}, 221 | hasListener: function hasListener() {}, 222 | hasListeners: function hasListeners() { 223 | return false 224 | }, 225 | removeListener: function removeListener() {} 226 | }) 227 | 228 | const response = { 229 | name: '', 230 | sender: undefined, 231 | disconnect: function disconnect() {}, 232 | onDisconnect: onSomething(), 233 | onMessage: onSomething(), 234 | postMessage: function postMessage() { 235 | if (!arguments.length) { 236 | throw new TypeError(`Insufficient number of arguments.`) 237 | } 238 | throw new Error(`Attempting to use a disconnected port object`) 239 | } 240 | } 241 | return response 242 | } 243 | }, 244 | { 245 | opts: this.opts, 246 | STATIC_DATA 247 | } 248 | ) 249 | } 250 | } 251 | 252 | module.exports = function(pluginConfig) { 253 | return new Plugin(pluginConfig) 254 | } 255 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/staticData.json: -------------------------------------------------------------------------------- 1 | { 2 | "OnInstalledReason": { 3 | "CHROME_UPDATE": "chrome_update", 4 | "INSTALL": "install", 5 | "SHARED_MODULE_UPDATE": "shared_module_update", 6 | "UPDATE": "update" 7 | }, 8 | "OnRestartRequiredReason": { 9 | "APP_UPDATE": "app_update", 10 | "OS_UPDATE": "os_update", 11 | "PERIODIC": "periodic" 12 | }, 13 | "PlatformArch": { 14 | "ARM": "arm", 15 | "ARM64": "arm64", 16 | "MIPS": "mips", 17 | "MIPS64": "mips64", 18 | "X86_32": "x86-32", 19 | "X86_64": "x86-64" 20 | }, 21 | "PlatformNaclArch": { 22 | "ARM": "arm", 23 | "MIPS": "mips", 24 | "MIPS64": "mips64", 25 | "X86_32": "x86-32", 26 | "X86_64": "x86-64" 27 | }, 28 | "PlatformOs": { 29 | "ANDROID": "android", 30 | "CROS": "cros", 31 | "LINUX": "linux", 32 | "MAC": "mac", 33 | "OPENBSD": "openbsd", 34 | "WIN": "win" 35 | }, 36 | "RequestUpdateCheckStatus": { 37 | "NO_UPDATE": "no_update", 38 | "THROTTLED": "throttled", 39 | "UPDATE_AVAILABLE": "update_available" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/defaultArgs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const argsToIgnore = [ 6 | '--disable-extensions', 7 | '--disable-default-apps', 8 | '--disable-component-extensions-with-background-pages' 9 | ] 10 | 11 | /** 12 | * A CDP driver like puppeteer can make use of various browser launch arguments that are 13 | * adversarial to mimicking a regular browser and need to be stripped when launching the browser. 14 | */ 15 | class Plugin extends PuppeteerExtraPlugin { 16 | constructor(opts = {}) { 17 | super(opts) 18 | } 19 | 20 | get name() { 21 | return 'stealth/evasions/defaultArgs' 22 | } 23 | 24 | get requirements() { 25 | return new Set(['runLast']) // So other plugins can modify launch options before 26 | } 27 | 28 | async beforeLaunch(options = {}) { 29 | options.ignoreDefaultArgs = options.ignoreDefaultArgs || [] 30 | if (options.ignoreDefaultArgs === true) { 31 | // that means the user explicitly wants to disable all default arguments 32 | return 33 | } 34 | argsToIgnore.forEach(arg => { 35 | if (options.ignoreDefaultArgs.includes(arg)) { 36 | return 37 | } 38 | options.ignoreDefaultArgs.push(arg) 39 | }) 40 | } 41 | } 42 | 43 | module.exports = function (pluginConfig) { 44 | return new Plugin(pluginConfig) 45 | } 46 | 47 | module.exports.argsToIgnore = argsToIgnore 48 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/defaultArgs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Fix for the HEADCHR_IFRAME detection (iframe.contentWindow.chrome), hopefully this time without breaking iframes. 9 | * Note: Only `srcdoc` powered iframes cause issues due to a chromium bug: 10 | * 11 | * https://github.com/puppeteer/puppeteer/issues/1106 12 | */ 13 | class Plugin extends PuppeteerExtraPlugin { 14 | constructor(opts = {}) { 15 | super(opts) 16 | } 17 | 18 | get name() { 19 | return 'stealth/evasions/iframe.contentWindow' 20 | } 21 | 22 | get requirements() { 23 | // Make sure `chrome.runtime` has ran, we use data defined by it (e.g. `window.chrome`) 24 | return new Set(['runLast']) 25 | } 26 | 27 | async onPageCreated(page) { 28 | await withUtils(page).evaluateOnNewDocument((utils, opts) => { 29 | try { 30 | // Adds a contentWindow proxy to the provided iframe element 31 | const addContentWindowProxy = iframe => { 32 | const contentWindowProxy = { 33 | get(target, key) { 34 | // Now to the interesting part: 35 | // We actually make this thing behave like a regular iframe window, 36 | // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :) 37 | // That makes it possible for these assertions to be correct: 38 | // iframe.contentWindow.self === window.top // must be false 39 | if (key === 'self') { 40 | return this 41 | } 42 | // iframe.contentWindow.frameElement === iframe // must be true 43 | if (key === 'frameElement') { 44 | return iframe 45 | } 46 | // Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy. 47 | if (key === '0') { 48 | return undefined 49 | } 50 | return Reflect.get(target, key) 51 | } 52 | } 53 | 54 | if (!iframe.contentWindow) { 55 | const proxy = new Proxy(window, contentWindowProxy) 56 | Object.defineProperty(iframe, 'contentWindow', { 57 | get() { 58 | return proxy 59 | }, 60 | set(newValue) { 61 | return newValue // contentWindow is immutable 62 | }, 63 | enumerable: true, 64 | configurable: false 65 | }) 66 | } 67 | } 68 | 69 | // Handles iframe element creation, augments `srcdoc` property so we can intercept further 70 | const handleIframeCreation = (target, thisArg, args) => { 71 | const iframe = target.apply(thisArg, args) 72 | 73 | // We need to keep the originals around 74 | const _iframe = iframe 75 | const _srcdoc = _iframe.srcdoc 76 | 77 | // Add hook for the srcdoc property 78 | // We need to be very surgical here to not break other iframes by accident 79 | Object.defineProperty(iframe, 'srcdoc', { 80 | configurable: true, // Important, so we can reset this later 81 | get: function() { 82 | return _srcdoc 83 | }, 84 | set: function(newValue) { 85 | addContentWindowProxy(this) 86 | // Reset property, the hook is only needed once 87 | Object.defineProperty(iframe, 'srcdoc', { 88 | configurable: false, 89 | writable: false, 90 | value: _srcdoc 91 | }) 92 | _iframe.srcdoc = newValue 93 | } 94 | }) 95 | return iframe 96 | } 97 | 98 | // Adds a hook to intercept iframe creation events 99 | const addIframeCreationSniffer = () => { 100 | /* global document */ 101 | const createElementHandler = { 102 | // Make toString() native 103 | get(target, key) { 104 | return Reflect.get(target, key) 105 | }, 106 | apply: function(target, thisArg, args) { 107 | const isIframe = 108 | args && args.length && `${args[0]}`.toLowerCase() === 'iframe' 109 | if (!isIframe) { 110 | // Everything as usual 111 | return target.apply(thisArg, args) 112 | } else { 113 | return handleIframeCreation(target, thisArg, args) 114 | } 115 | } 116 | } 117 | // All this just due to iframes with srcdoc bug 118 | utils.replaceWithProxy( 119 | document, 120 | 'createElement', 121 | createElementHandler 122 | ) 123 | } 124 | 125 | // Let's go 126 | addIframeCreationSniffer() 127 | } catch (err) { 128 | // console.warn(err) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | module.exports = function(pluginConfig) { 135 | return new Plugin(pluginConfig) 136 | } 137 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/media.codecs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Fix Chromium not reporting "probably" to codecs like `videoEl.canPlayType('video/mp4; codecs="avc1.42E01E"')`. 9 | * (Chromium doesn't support proprietary codecs, only Chrome does) 10 | */ 11 | class Plugin extends PuppeteerExtraPlugin { 12 | constructor(opts = {}) { 13 | super(opts) 14 | } 15 | 16 | get name() { 17 | return 'stealth/evasions/media.codecs' 18 | } 19 | 20 | async onPageCreated(page) { 21 | await withUtils(page).evaluateOnNewDocument(utils => { 22 | /** 23 | * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing. 24 | * 25 | * @example 26 | * video/webm; codecs="vp8, vorbis" 27 | * video/mp4; codecs="avc1.42E01E" 28 | * audio/x-m4a; 29 | * audio/ogg; codecs="vorbis" 30 | * @param {String} arg 31 | */ 32 | const parseInput = arg => { 33 | const [mime, codecStr] = arg.trim().split(';') 34 | let codecs = [] 35 | if (codecStr && codecStr.includes('codecs="')) { 36 | codecs = codecStr 37 | .trim() 38 | .replace(`codecs="`, '') 39 | .replace(`"`, '') 40 | .trim() 41 | .split(',') 42 | .filter(x => !!x) 43 | .map(x => x.trim()) 44 | } 45 | return { 46 | mime, 47 | codecStr, 48 | codecs 49 | } 50 | } 51 | 52 | const canPlayType = { 53 | // Intercept certain requests 54 | apply: function(target, ctx, args) { 55 | if (!args || !args.length) { 56 | return target.apply(ctx, args) 57 | } 58 | const { mime, codecs } = parseInput(args[0]) 59 | // This specific mp4 codec is missing in Chromium 60 | if (mime === 'video/mp4') { 61 | if (codecs.includes('avc1.42E01E')) { 62 | return 'probably' 63 | } 64 | } 65 | // This mimetype is only supported if no codecs are specified 66 | if (mime === 'audio/x-m4a' && !codecs.length) { 67 | return 'maybe' 68 | } 69 | 70 | // This mimetype is only supported if no codecs are specified 71 | if (mime === 'audio/aac' && !codecs.length) { 72 | return 'probably' 73 | } 74 | // Everything else as usual 75 | return target.apply(ctx, args) 76 | } 77 | } 78 | 79 | /* global HTMLMediaElement */ 80 | utils.replaceWithProxy( 81 | HTMLMediaElement.prototype, 82 | 'canPlayType', 83 | canPlayType 84 | ) 85 | }) 86 | } 87 | } 88 | 89 | module.exports = function(pluginConfig) { 90 | return new Plugin(pluginConfig) 91 | } 92 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/media.codecs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Set the hardwareConcurrency to 4 (optionally configurable with `hardwareConcurrency`) 9 | * 10 | * @see https://arh.antoinevastel.com/reports/stats/osName_hardwareConcurrency_report.html 11 | * 12 | * @param {Object} [opts] - Options 13 | * @param {number} [opts.hardwareConcurrency] - The value to use in `navigator.hardwareConcurrency` (default: `4`) 14 | */ 15 | 16 | class Plugin extends PuppeteerExtraPlugin { 17 | constructor(opts = {}) { 18 | super(opts) 19 | } 20 | 21 | get name() { 22 | return 'stealth/evasions/navigator.hardwareConcurrency' 23 | } 24 | 25 | get defaults() { 26 | return { 27 | hardwareConcurrency: 4 28 | } 29 | } 30 | 31 | async onPageCreated(page) { 32 | await withUtils(page).evaluateOnNewDocument( 33 | (utils, { opts }) => { 34 | utils.replaceGetterWithProxy( 35 | Object.getPrototypeOf(navigator), 36 | 'hardwareConcurrency', 37 | utils.makeHandler().getterValue(opts.hardwareConcurrency) 38 | ) 39 | }, 40 | { 41 | opts: this.opts 42 | } 43 | ) 44 | } 45 | } 46 | 47 | module.exports = function (pluginConfig) { 48 | return new Plugin(pluginConfig) 49 | } 50 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.languages/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | const withUtils = require('../_utils/withUtils') 5 | 6 | /** 7 | * Pass the Languages Test. Allows setting custom languages. 8 | * 9 | * @param {Object} [opts] - Options 10 | * @param {Array} [opts.languages] - The languages to use (default: `['en-US', 'en']`) 11 | */ 12 | class Plugin extends PuppeteerExtraPlugin { 13 | constructor(opts = {}) { 14 | super(opts) 15 | } 16 | 17 | get name() { 18 | return 'stealth/evasions/navigator.languages' 19 | } 20 | 21 | get defaults() { 22 | return { 23 | languages: [] // Empty default, otherwise this would be merged with user defined array override 24 | } 25 | } 26 | 27 | async onPageCreated(page) { 28 | await withUtils(page).evaluateOnNewDocument( 29 | (utils, { opts }) => { 30 | const languages = opts.languages.length 31 | ? opts.languages 32 | : ['en-US', 'en'] 33 | utils.replaceGetterWithProxy( 34 | Object.getPrototypeOf(navigator), 35 | 'languages', 36 | utils.makeHandler().getterValue(Object.freeze([...languages])) 37 | ) 38 | }, 39 | { 40 | opts: this.opts 41 | } 42 | ) 43 | } 44 | } 45 | 46 | module.exports = function (pluginConfig) { 47 | return new Plugin(pluginConfig) 48 | } 49 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.languages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.permissions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Fix `Notification.permission` behaving weirdly in headless mode 9 | * 10 | * @see https://bugs.chromium.org/p/chromium/issues/detail?id=1052332 11 | */ 12 | 13 | class Plugin extends PuppeteerExtraPlugin { 14 | constructor(opts = {}) { 15 | super(opts) 16 | } 17 | 18 | get name() { 19 | return 'stealth/evasions/navigator.permissions' 20 | } 21 | 22 | /* global Notification Permissions PermissionStatus */ 23 | async onPageCreated(page) { 24 | await withUtils(page).evaluateOnNewDocument((utils, opts) => { 25 | const isSecure = document.location.protocol.startsWith('https') 26 | 27 | // In headful on secure origins the permission should be "default", not "denied" 28 | if (isSecure) { 29 | utils.replaceGetterWithProxy(Notification, 'permission', { 30 | apply() { 31 | return 'default' 32 | } 33 | }) 34 | } 35 | 36 | // Another weird behavior: 37 | // On insecure origins in headful the state is "denied", 38 | // whereas in headless it's "prompt" 39 | if (!isSecure) { 40 | const handler = { 41 | apply(target, ctx, args) { 42 | const param = (args || [])[0] 43 | 44 | const isNotifications = 45 | param && param.name && param.name === 'notifications' 46 | if (!isNotifications) { 47 | return utils.cache.Reflect.apply(...arguments) 48 | } 49 | 50 | return Promise.resolve( 51 | Object.setPrototypeOf( 52 | { 53 | state: 'denied', 54 | onchange: null 55 | }, 56 | PermissionStatus.prototype 57 | ) 58 | ) 59 | } 60 | } 61 | // Note: Don't use `Object.getPrototypeOf` here 62 | utils.replaceWithProxy(Permissions.prototype, 'query', handler) 63 | } 64 | }, this.opts) 65 | } 66 | } 67 | 68 | module.exports = function (pluginConfig) { 69 | return new Plugin(pluginConfig) 70 | } 71 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.permissions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "mimeTypes": [ 3 | { 4 | "type": "application/pdf", 5 | "suffixes": "pdf", 6 | "description": "", 7 | "__pluginName": "Chrome PDF Viewer" 8 | }, 9 | { 10 | "type": "application/x-google-chrome-pdf", 11 | "suffixes": "pdf", 12 | "description": "Portable Document Format", 13 | "__pluginName": "Chrome PDF Plugin" 14 | }, 15 | { 16 | "type": "application/x-nacl", 17 | "suffixes": "", 18 | "description": "Native Client Executable", 19 | "__pluginName": "Native Client" 20 | }, 21 | { 22 | "type": "application/x-pnacl", 23 | "suffixes": "", 24 | "description": "Portable Native Client Executable", 25 | "__pluginName": "Native Client" 26 | } 27 | ], 28 | "plugins": [ 29 | { 30 | "name": "Chrome PDF Plugin", 31 | "filename": "internal-pdf-viewer", 32 | "description": "Portable Document Format", 33 | "__mimeTypes": ["application/x-google-chrome-pdf"] 34 | }, 35 | { 36 | "name": "Chrome PDF Viewer", 37 | "filename": "mhjfbmdgcfjbbpaeojofohoefgiehjai", 38 | "description": "", 39 | "__mimeTypes": ["application/pdf"] 40 | }, 41 | { 42 | "name": "Native Client", 43 | "filename": "internal-nacl-plugin", 44 | "description": "", 45 | "__mimeTypes": ["application/x-nacl", "application/x-pnacl"] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/functionMocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `navigator.{plugins,mimeTypes}` share similar custom functions to look up properties 3 | * 4 | * Note: This is meant to be run in the context of the page. 5 | */ 6 | module.exports.generateFunctionMocks = utils => ( 7 | proto, 8 | itemMainProp, 9 | dataArray 10 | ) => ({ 11 | /** Returns the MimeType object with the specified index. */ 12 | item: utils.createProxy(proto.item, { 13 | apply(target, ctx, args) { 14 | if (!args.length) { 15 | throw new TypeError( 16 | `Failed to execute 'item' on '${ 17 | proto[Symbol.toStringTag] 18 | }': 1 argument required, but only 0 present.` 19 | ) 20 | } 21 | // Special behavior alert: 22 | // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup 23 | // - If anything else than an integer (including as string) is provided it will return the first entry 24 | const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer 25 | // Note: Vanilla never returns `undefined` 26 | return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null 27 | } 28 | }), 29 | /** Returns the MimeType object with the specified name. */ 30 | namedItem: utils.createProxy(proto.namedItem, { 31 | apply(target, ctx, args) { 32 | if (!args.length) { 33 | throw new TypeError( 34 | `Failed to execute 'namedItem' on '${ 35 | proto[Symbol.toStringTag] 36 | }': 1 argument required, but only 0 present.` 37 | ) 38 | } 39 | return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`! 40 | } 41 | }), 42 | /** Does nothing and shall return nothing */ 43 | refresh: proto.refresh 44 | ? utils.createProxy(proto.refresh, { 45 | apply(target, ctx, args) { 46 | return undefined 47 | } 48 | }) 49 | : undefined 50 | }) 51 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const utils = require('../_utils') 6 | const withUtils = require('../_utils/withUtils') 7 | 8 | const { generateMimeTypeArray } = require('./mimeTypes') 9 | const { generatePluginArray } = require('./plugins') 10 | const { generateMagicArray } = require('./magicArray') 11 | const { generateFunctionMocks } = require('./functionMocks') 12 | 13 | const data = require('./data.json') 14 | 15 | /** 16 | * In headless mode `navigator.mimeTypes` and `navigator.plugins` are empty. 17 | * This plugin emulates both of these with functional mocks to match regular headful Chrome. 18 | * 19 | * Note: mimeTypes and plugins cross-reference each other, so it makes sense to do them at the same time. 20 | * 21 | * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/mimeTypes 22 | * @see https://developer.mozilla.org/en-US/docs/Web/API/MimeTypeArray 23 | * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/plugins 24 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PluginArray 25 | */ 26 | class Plugin extends PuppeteerExtraPlugin { 27 | constructor(opts = {}) { 28 | super(opts) 29 | } 30 | 31 | get name() { 32 | return 'stealth/evasions/navigator.plugins' 33 | } 34 | 35 | async onPageCreated(page) { 36 | await withUtils(page).evaluateOnNewDocument( 37 | (utils, { fns, data }) => { 38 | fns = utils.materializeFns(fns) 39 | 40 | // That means we're running headful 41 | const hasPlugins = 'plugins' in navigator && navigator.plugins.length 42 | if (hasPlugins) { 43 | return // nothing to do here 44 | } 45 | 46 | const mimeTypes = fns.generateMimeTypeArray(utils, fns)(data.mimeTypes) 47 | const plugins = fns.generatePluginArray(utils, fns)(data.plugins) 48 | 49 | // Plugin and MimeType cross-reference each other, let's do that now 50 | // Note: We're looping through `data.plugins` here, not the generated `plugins` 51 | for (const pluginData of data.plugins) { 52 | pluginData.__mimeTypes.forEach((type, index) => { 53 | plugins[pluginData.name][index] = mimeTypes[type] 54 | 55 | Object.defineProperty(plugins[pluginData.name], type, { 56 | value: mimeTypes[type], 57 | writable: false, 58 | enumerable: false, // Not enumerable 59 | configurable: true 60 | }) 61 | Object.defineProperty(mimeTypes[type], 'enabledPlugin', { 62 | value: 63 | type === 'application/x-pnacl' 64 | ? mimeTypes['application/x-nacl'].enabledPlugin // these reference the same plugin, so we need to re-use the Proxy in order to avoid leaks 65 | : new Proxy(plugins[pluginData.name], {}), // Prevent circular references 66 | writable: false, 67 | enumerable: false, // Important: `JSON.stringify(navigator.plugins)` 68 | configurable: true 69 | }) 70 | }) 71 | } 72 | 73 | const patchNavigator = (name, value) => 74 | utils.replaceProperty(Object.getPrototypeOf(navigator), name, { 75 | get() { 76 | return value 77 | } 78 | }) 79 | 80 | patchNavigator('mimeTypes', mimeTypes) 81 | patchNavigator('plugins', plugins) 82 | 83 | // All done 84 | }, 85 | { 86 | // We pass some functions to evaluate to structure the code more nicely 87 | fns: utils.stringifyFns({ 88 | generateMimeTypeArray, 89 | generatePluginArray, 90 | generateMagicArray, 91 | generateFunctionMocks 92 | }), 93 | data 94 | } 95 | ) 96 | } 97 | } 98 | 99 | module.exports = function(pluginConfig) { 100 | return new Plugin(pluginConfig) 101 | } 102 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/magicArray.js: -------------------------------------------------------------------------------- 1 | /* global MimeType MimeTypeArray Plugin PluginArray */ 2 | 3 | /** 4 | * Generate a convincing and functional MimeType or Plugin array from scratch. 5 | * They're so similar that it makes sense to use a single generator here. 6 | * 7 | * Note: This is meant to be run in the context of the page. 8 | */ 9 | module.exports.generateMagicArray = (utils, fns) => 10 | function( 11 | dataArray = [], 12 | proto = MimeTypeArray.prototype, 13 | itemProto = MimeType.prototype, 14 | itemMainProp = 'type' 15 | ) { 16 | // Quick helper to set props with the same descriptors vanilla is using 17 | const defineProp = (obj, prop, value) => 18 | Object.defineProperty(obj, prop, { 19 | value, 20 | writable: false, 21 | enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)` 22 | configurable: true 23 | }) 24 | 25 | // Loop over our fake data and construct items 26 | const makeItem = data => { 27 | const item = {} 28 | for (const prop of Object.keys(data)) { 29 | if (prop.startsWith('__')) { 30 | continue 31 | } 32 | defineProp(item, prop, data[prop]) 33 | } 34 | return patchItem(item, data) 35 | } 36 | 37 | const patchItem = (item, data) => { 38 | let descriptor = Object.getOwnPropertyDescriptors(item) 39 | 40 | // Special case: Plugins have a magic length property which is not enumerable 41 | // e.g. `navigator.plugins[i].length` should always be the length of the assigned mimeTypes 42 | if (itemProto === Plugin.prototype) { 43 | descriptor = { 44 | ...descriptor, 45 | length: { 46 | value: data.__mimeTypes.length, 47 | writable: false, 48 | enumerable: false, 49 | configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length` 50 | } 51 | } 52 | } 53 | 54 | // We need to spoof a specific `MimeType` or `Plugin` object 55 | const obj = Object.create(itemProto, descriptor) 56 | 57 | // Virtually all property keys are not enumerable in vanilla 58 | const blacklist = [...Object.keys(data), 'length', 'enabledPlugin'] 59 | return new Proxy(obj, { 60 | ownKeys(target) { 61 | return Reflect.ownKeys(target).filter(k => !blacklist.includes(k)) 62 | }, 63 | getOwnPropertyDescriptor(target, prop) { 64 | if (blacklist.includes(prop)) { 65 | return undefined 66 | } 67 | return Reflect.getOwnPropertyDescriptor(target, prop) 68 | } 69 | }) 70 | } 71 | 72 | const magicArray = [] 73 | 74 | // Loop through our fake data and use that to create convincing entities 75 | dataArray.forEach(data => { 76 | magicArray.push(makeItem(data)) 77 | }) 78 | 79 | // Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards 80 | magicArray.forEach(entry => { 81 | defineProp(magicArray, entry[itemMainProp], entry) 82 | }) 83 | 84 | // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)` 85 | const magicArrayObj = Object.create(proto, { 86 | ...Object.getOwnPropertyDescriptors(magicArray), 87 | 88 | // There's one ugly quirk we unfortunately need to take care of: 89 | // The `MimeTypeArray` prototype has an enumerable `length` property, 90 | // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`. 91 | // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap. 92 | length: { 93 | value: magicArray.length, 94 | writable: false, 95 | enumerable: false, 96 | configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length` 97 | } 98 | }) 99 | 100 | // Generate our functional function mocks :-) 101 | const functionMocks = fns.generateFunctionMocks(utils)( 102 | proto, 103 | itemMainProp, 104 | magicArray 105 | ) 106 | 107 | // We need to overlay our custom object with a JS Proxy 108 | const magicArrayObjProxy = new Proxy(magicArrayObj, { 109 | get(target, key = '') { 110 | // Redirect function calls to our custom proxied versions mocking the vanilla behavior 111 | if (key === 'item') { 112 | return functionMocks.item 113 | } 114 | if (key === 'namedItem') { 115 | return functionMocks.namedItem 116 | } 117 | if (proto === PluginArray.prototype && key === 'refresh') { 118 | return functionMocks.refresh 119 | } 120 | // Everything else can pass through as normal 121 | return utils.cache.Reflect.get(...arguments) 122 | }, 123 | ownKeys(target) { 124 | // There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense 125 | // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length` 126 | // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly 127 | // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing 128 | // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing 129 | const keys = [] 130 | const typeProps = magicArray.map(mt => mt[itemMainProp]) 131 | typeProps.forEach((_, i) => keys.push(`${i}`)) 132 | typeProps.forEach(propName => keys.push(propName)) 133 | return keys 134 | }, 135 | getOwnPropertyDescriptor(target, prop) { 136 | if (prop === 'length') { 137 | return undefined 138 | } 139 | return Reflect.getOwnPropertyDescriptor(target, prop) 140 | } 141 | }) 142 | 143 | return magicArrayObjProxy 144 | } 145 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/mimeTypes.js: -------------------------------------------------------------------------------- 1 | /* global MimeType MimeTypeArray */ 2 | 3 | /** 4 | * Generate a convincing and functional MimeTypeArray (with mime types) from scratch. 5 | * 6 | * Note: This is meant to be run in the context of the page. 7 | * 8 | * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/mimeTypes 9 | * @see https://developer.mozilla.org/en-US/docs/Web/API/MimeTypeArray 10 | */ 11 | module.exports.generateMimeTypeArray = (utils, fns) => mimeTypesData => { 12 | return fns.generateMagicArray(utils, fns)( 13 | mimeTypesData, 14 | MimeTypeArray.prototype, 15 | MimeType.prototype, 16 | 'type' 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/plugins.js: -------------------------------------------------------------------------------- 1 | /* global Plugin PluginArray */ 2 | 3 | /** 4 | * Generate a convincing and functional PluginArray (with plugins) from scratch. 5 | * 6 | * Note: This is meant to be run in the context of the page. 7 | * 8 | * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/plugins 9 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PluginArray 10 | */ 11 | module.exports.generatePluginArray = (utils, fns) => pluginsData => { 12 | return fns.generateMagicArray(utils, fns)( 13 | pluginsData, 14 | PluginArray.prototype, 15 | Plugin.prototype, 16 | 'name' 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.vendor/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * By default puppeteer will have a fixed `navigator.vendor` property. 9 | * 10 | * This plugin makes it possible to change this property. 11 | * 12 | * @example 13 | * const puppeteer = require("puppeteer-extra") 14 | * 15 | * const StealthPlugin = require("puppeteer-extra-plugin-stealth") 16 | * const stealth = StealthPlugin() 17 | * // Remove this specific stealth plugin from the default set 18 | * stealth.enabledEvasions.delete("navigator.vendor") 19 | * puppeteer.use(stealth) 20 | * 21 | * // Stealth plugins are just regular `puppeteer-extra` plugins and can be added as such 22 | * const NavigatorVendorPlugin = require("puppeteer-extra-plugin-stealth/evasions/navigator.vendor") 23 | * const nvp = NavigatorVendorPlugin({ vendor: 'Apple Computer, Inc.' }) // Custom vendor 24 | * puppeteer.use(nvp) 25 | * 26 | * @param {Object} [opts] - Options 27 | * @param {string} [opts.vendor] - The vendor to use in `navigator.vendor` (default: `Google Inc.`) 28 | * 29 | */ 30 | class Plugin extends PuppeteerExtraPlugin { 31 | constructor(opts = {}) { 32 | super(opts) 33 | } 34 | 35 | get name() { 36 | return 'stealth/evasions/navigator.vendor' 37 | } 38 | 39 | get defaults() { 40 | return { 41 | vendor: 'Google Inc.' 42 | } 43 | } 44 | 45 | async onPageCreated(page) { 46 | this.debug('onPageCreated', { 47 | opts: this.opts 48 | }) 49 | 50 | await withUtils(page).evaluateOnNewDocument( 51 | (utils, { opts }) => { 52 | utils.replaceGetterWithProxy( 53 | Object.getPrototypeOf(navigator), 54 | 'vendor', 55 | utils.makeHandler().getterValue(opts.vendor) 56 | ) 57 | }, 58 | { 59 | opts: this.opts 60 | } 61 | ) 62 | } // onPageCreated 63 | } 64 | 65 | const defaultExport = opts => new Plugin(opts) 66 | module.exports = defaultExport 67 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.vendor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.webdriver/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | /** 6 | * Pass the Webdriver Test. 7 | * Will delete `navigator.webdriver` property. 8 | */ 9 | class Plugin extends PuppeteerExtraPlugin { 10 | constructor(opts = {}) { 11 | super(opts) 12 | } 13 | 14 | get name() { 15 | return 'stealth/evasions/navigator.webdriver' 16 | } 17 | 18 | async onPageCreated(page) { 19 | await page.evaluateOnNewDocument(() => { 20 | if (navigator.webdriver === false) { 21 | // Post Chrome 89.0.4339.0 and already good 22 | } else if (navigator.webdriver === undefined) { 23 | // Pre Chrome 89.0.4339.0 and already good 24 | } else { 25 | // Pre Chrome 88.0.4291.0 and needs patching 26 | delete Object.getPrototypeOf(navigator).webdriver 27 | } 28 | }) 29 | } 30 | 31 | // Post Chrome 88.0.4291.0 32 | // Note: this will add an infobar to Chrome with a warning that an unsupported flag is set 33 | // To remove this bar on Linux, run: mkdir -p /etc/opt/chrome/policies/managed && echo '{ "CommandLineFlagSecurityWarningsEnabled": false }' > /etc/opt/chrome/policies/managed/managed_policies.json 34 | async beforeLaunch(options) { 35 | // If disable-blink-features is already passed, append the AutomationControlled switch 36 | const idx = options.args.findIndex((arg) => arg.startsWith('--disable-blink-features=')); 37 | if (idx !== -1) { 38 | const arg = options.args[idx]; 39 | options.args[idx] = `${arg},AutomationControlled`; 40 | } else { 41 | options.args.push('--disable-blink-features=AutomationControlled'); 42 | } 43 | } 44 | } 45 | 46 | module.exports = function(pluginConfig) { 47 | return new Plugin(pluginConfig) 48 | } 49 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/navigator.webdriver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/sourceurl/_fixtures/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Title 6 | 7 | 8 | 9 |

Please use `document.querySelector`..

10 | 11 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/sourceurl/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | /** 6 | * Strip sourceURL from scripts injected by puppeteer. 7 | * It can be used to identify the presence of pptr via stacktraces. 8 | */ 9 | class Plugin extends PuppeteerExtraPlugin { 10 | constructor(opts = {}) { 11 | super(opts) 12 | } 13 | 14 | get name() { 15 | return 'stealth/evasions/sourceurl' 16 | } 17 | 18 | async onPageCreated(page) { 19 | const client = 20 | page && typeof page._client === 'function' ? page._client() : page._client 21 | if (!client) { 22 | this.debug('Warning, missing properties to intercept CDP.', { page }) 23 | return 24 | } 25 | 26 | // Intercept CDP commands and strip identifying and unnecessary sourceURL 27 | // https://github.com/puppeteer/puppeteer/blob/9b3005c105995cd267fdc7fb95b78aceab82cf0e/new-docs/puppeteer.cdpsession.md 28 | const debug = this.debug 29 | client.send = (function(originalMethod, context) { 30 | return async function() { 31 | const [method, paramArgs] = arguments || [] 32 | const next = async () => { 33 | try { 34 | return await originalMethod.apply(context, [method, paramArgs]) 35 | } catch (error) { 36 | // This seems to happen sometimes when redirects cause other outstanding requests to be cut short 37 | if ( 38 | error instanceof Error && 39 | error.message.includes( 40 | `Protocol error (Network.getResponseBody): No resource with given identifier found` 41 | ) 42 | ) { 43 | debug( 44 | `Caught and ignored an error about a missing network resource.`, 45 | { error } 46 | ) 47 | } else { 48 | throw error 49 | } 50 | } 51 | } 52 | 53 | if (!method || !paramArgs) { 54 | return next() 55 | } 56 | 57 | // To find the methods/props in question check `_evaluateInternal` at: 58 | // https://github.com/puppeteer/puppeteer/blob/main/src/common/ExecutionContext.ts#L186 59 | const methodsToPatch = { 60 | 'Runtime.evaluate': 'expression', 61 | 'Runtime.callFunctionOn': 'functionDeclaration' 62 | } 63 | const SOURCE_URL_SUFFIX = 64 | '//# sourceURL=__puppeteer_evaluation_script__' 65 | 66 | if (!methodsToPatch[method] || !paramArgs[methodsToPatch[method]]) { 67 | return next() 68 | } 69 | 70 | debug('Stripping sourceURL', { method }) 71 | paramArgs[methodsToPatch[method]] = paramArgs[ 72 | methodsToPatch[method] 73 | ].replace(SOURCE_URL_SUFFIX, '') 74 | 75 | return next() 76 | } 77 | })(client.send, client) 78 | } 79 | } 80 | 81 | module.exports = function(pluginConfig) { 82 | return new Plugin(pluginConfig) 83 | } 84 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/sourceurl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/user-agent-override/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | /** 6 | * Fixes the UserAgent info (composed of UA string, Accept-Language, Platform, and UA hints). 7 | * 8 | * If you don't provide any values this plugin will default to using the regular UserAgent string (while stripping the headless part). 9 | * Default language is set to "en-US,en", the other settings match the UserAgent string. 10 | * If you are running on Linux, it will mask the settins to look like Windows. This behavior can be disabled with the `maskLinux` option. 11 | * 12 | * By default puppeteer will not set a `Accept-Language` header in headless: 13 | * It's (theoretically) possible to fix that using either `page.setExtraHTTPHeaders` or a `--lang` launch arg. 14 | * Unfortunately `page.setExtraHTTPHeaders` will lowercase everything and launch args are not always available. :) 15 | * 16 | * In addition, the `navigator.platform` property is always set to the host value, e.g. `Linux` which makes detection very easy. 17 | * 18 | * Note: You cannot use the regular `page.setUserAgent()` puppeteer call in your code, 19 | * as it will reset the language and platform values you set with this plugin. 20 | * 21 | * @example 22 | * const puppeteer = require("puppeteer-extra") 23 | * 24 | * const StealthPlugin = require("puppeteer-extra-plugin-stealth") 25 | * const stealth = StealthPlugin() 26 | * // Remove this specific stealth plugin from the default set 27 | * stealth.enabledEvasions.delete("user-agent-override") 28 | * puppeteer.use(stealth) 29 | * 30 | * // Stealth plugins are just regular `puppeteer-extra` plugins and can be added as such 31 | * const UserAgentOverride = require("puppeteer-extra-plugin-stealth/evasions/user-agent-override") 32 | * // Define custom UA and locale 33 | * const ua = UserAgentOverride({ userAgent: "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", locale: "de-DE,de" }) 34 | * puppeteer.use(ua) 35 | * 36 | * @param {Object} [opts] - Options 37 | * @param {string} [opts.userAgent] - The user agent to use (default: browser.userAgent()) 38 | * @param {string} [opts.locale] - The locale to use in `Accept-Language` header and in `navigator.languages` (default: `en-US,en`) 39 | * @param {boolean} [opts.maskLinux] - Wether to hide Linux as platform in the user agent or not - true by default 40 | * 41 | */ 42 | class Plugin extends PuppeteerExtraPlugin { 43 | constructor(opts = {}) { 44 | super(opts) 45 | 46 | this._headless = false 47 | } 48 | 49 | get name() { 50 | return 'stealth/evasions/user-agent-override' 51 | } 52 | 53 | get dependencies() { 54 | return new Set(['user-preferences']) 55 | } 56 | 57 | get defaults() { 58 | return { 59 | userAgent: null, 60 | locale: 'en-US,en', 61 | maskLinux: true 62 | } 63 | } 64 | 65 | async onPageCreated(page) { 66 | // Determine the full user agent string, strip the "Headless" part 67 | let ua = 68 | this.opts.userAgent || 69 | (await page.browser().userAgent()).replace('HeadlessChrome/', 'Chrome/') 70 | 71 | if ( 72 | this.opts.maskLinux && 73 | ua.includes('Linux') && 74 | !ua.includes('Android') // Skip Android user agents since they also contain Linux 75 | ) { 76 | ua = ua.replace(/\(([^)]+)\)/, '(Windows NT 10.0; Win64; x64)') // Replace the first part in parentheses with Windows data 77 | } 78 | 79 | // Full version number from Chrome 80 | const uaVersion = ua.includes('Chrome/') 81 | ? ua.match(/Chrome\/([\d|.]+)/)[1] 82 | : (await page.browser().version()).match(/\/([\d|.]+)/)[1] 83 | 84 | // Get platform identifier (short or long version) 85 | const _getPlatform = (extended = false) => { 86 | if (ua.includes('Mac OS X')) { 87 | return extended ? 'Mac OS X' : 'MacIntel' 88 | } else if (ua.includes('Android')) { 89 | return 'Android' 90 | } else if (ua.includes('Linux')) { 91 | return 'Linux' 92 | } else { 93 | return extended ? 'Windows' : 'Win32' 94 | } 95 | } 96 | 97 | // Source in C++: https://source.chromium.org/chromium/chromium/src/+/master:components/embedder_support/user_agent_utils.cc;l=55-100 98 | const _getBrands = () => { 99 | const seed = uaVersion.split('.')[0] // the major version number of Chrome 100 | 101 | const order = [ 102 | [0, 1, 2], 103 | [0, 2, 1], 104 | [1, 0, 2], 105 | [1, 2, 0], 106 | [2, 0, 1], 107 | [2, 1, 0] 108 | ][seed % 6] 109 | const escapedChars = [' ', ' ', ';'] 110 | 111 | const greaseyBrand = `${escapedChars[order[0]]}Not${ 112 | escapedChars[order[1]] 113 | }A${escapedChars[order[2]]}Brand` 114 | 115 | const greasedBrandVersionList = [] 116 | greasedBrandVersionList[order[0]] = { 117 | brand: greaseyBrand, 118 | version: '99' 119 | } 120 | greasedBrandVersionList[order[1]] = { 121 | brand: 'Chromium', 122 | version: seed 123 | } 124 | greasedBrandVersionList[order[2]] = { 125 | brand: 'Google Chrome', 126 | version: seed 127 | } 128 | 129 | return greasedBrandVersionList 130 | } 131 | 132 | // Return OS version 133 | const _getPlatformVersion = () => { 134 | if (ua.includes('Mac OS X ')) { 135 | return ua.match(/Mac OS X ([^)]+)/)[1] 136 | } else if (ua.includes('Android ')) { 137 | return ua.match(/Android ([^;]+)/)[1] 138 | } else if (ua.includes('Windows ')) { 139 | return ua.match(/Windows .*?([\d|.]+);?/)[1] 140 | } else { 141 | return '' 142 | } 143 | } 144 | 145 | // Get architecture, this seems to be empty on mobile and x86 on desktop 146 | const _getPlatformArch = () => (_getMobile() ? '' : 'x86') 147 | 148 | // Return the Android model, empty on desktop 149 | const _getPlatformModel = () => 150 | _getMobile() ? ua.match(/Android.*?;\s([^)]+)/)[1] : '' 151 | 152 | const _getMobile = () => ua.includes('Android') 153 | 154 | const override = { 155 | userAgent: ua, 156 | platform: _getPlatform(), 157 | userAgentMetadata: { 158 | brands: _getBrands(), 159 | fullVersion: uaVersion, 160 | platform: _getPlatform(true), 161 | platformVersion: _getPlatformVersion(), 162 | architecture: _getPlatformArch(), 163 | model: _getPlatformModel(), 164 | mobile: _getMobile() 165 | } 166 | } 167 | 168 | // In case of headless, override the acceptLanguage in CDP. 169 | // This is not preferred, as it messed up the header order. 170 | // On headful, we set the user preference language setting instead. 171 | if (this._headless) { 172 | override.acceptLanguage = this.opts.locale || 'en-US,en' 173 | } 174 | 175 | this.debug('onPageCreated - Will set these user agent options', { 176 | override, 177 | opts: this.opts 178 | }) 179 | 180 | const client = 181 | typeof page._client === 'function' ? page._client() : page._client 182 | client.send('Network.setUserAgentOverride', override) 183 | } 184 | 185 | async beforeLaunch(options) { 186 | // Check if launched headless 187 | this._headless = options.headless 188 | } 189 | 190 | async beforeConnect() { 191 | // Treat browsers using connect() as headless browsers 192 | this._headless = true 193 | } 194 | 195 | get data() { 196 | return [ 197 | { 198 | name: 'userPreferences', 199 | value: { 200 | intl: { accept_languages: this.opts.locale || 'en-US,en' } 201 | } 202 | } 203 | ] 204 | } 205 | } 206 | 207 | const defaultExport = opts => new Plugin(opts) 208 | module.exports = defaultExport 209 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/user-agent-override/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/webgl.vendor/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | const withUtils = require('../_utils/withUtils') 6 | 7 | /** 8 | * Fix WebGL Vendor/Renderer being set to Google in headless mode 9 | * 10 | * Example data (Apple Retina MBP 13): {vendor: "Intel Inc.", renderer: "Intel(R) Iris(TM) Graphics 6100"} 11 | * 12 | * @param {Object} [opts] - Options 13 | * @param {string} [opts.vendor] - The vendor string to use (default: `Intel Inc.`) 14 | * @param {string} [opts.renderer] - The renderer string (default: `Intel Iris OpenGL Engine`) 15 | */ 16 | class Plugin extends PuppeteerExtraPlugin { 17 | constructor(opts = {}) { 18 | super(opts) 19 | } 20 | 21 | get name() { 22 | return 'stealth/evasions/webgl.vendor' 23 | } 24 | 25 | /* global WebGLRenderingContext WebGL2RenderingContext */ 26 | async onPageCreated(page) { 27 | await withUtils(page).evaluateOnNewDocument((utils, opts) => { 28 | const getParameterProxyHandler = { 29 | apply: function(target, ctx, args) { 30 | const param = (args || [])[0] 31 | const result = utils.cache.Reflect.apply(target, ctx, args) 32 | // UNMASKED_VENDOR_WEBGL 33 | if (param === 37445) { 34 | return opts.vendor || 'Intel Inc.' // default in headless: Google Inc. 35 | } 36 | // UNMASKED_RENDERER_WEBGL 37 | if (param === 37446) { 38 | return opts.renderer || 'Intel Iris OpenGL Engine' // default in headless: Google SwiftShader 39 | } 40 | return result 41 | } 42 | } 43 | 44 | // There's more than one WebGL rendering context 45 | // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility 46 | // To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter) 47 | const addProxy = (obj, propName) => { 48 | utils.replaceWithProxy(obj, propName, getParameterProxyHandler) 49 | } 50 | // For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing: 51 | addProxy(WebGLRenderingContext.prototype, 'getParameter') 52 | addProxy(WebGL2RenderingContext.prototype, 'getParameter') 53 | }, this.opts) 54 | } 55 | } 56 | 57 | module.exports = function(pluginConfig) { 58 | return new Plugin(pluginConfig) 59 | } 60 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/webgl.vendor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/window.outerdimensions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') 4 | 5 | /** 6 | * Fix missing window.outerWidth/window.outerHeight in headless mode 7 | * Will also set the viewport to match window size, unless specified by user 8 | */ 9 | class Plugin extends PuppeteerExtraPlugin { 10 | constructor(opts = {}) { 11 | super(opts) 12 | } 13 | 14 | get name() { 15 | return 'stealth/evasions/window.outerdimensions' 16 | } 17 | 18 | async onPageCreated(page) { 19 | // Chrome returns undefined, Firefox false 20 | await page.evaluateOnNewDocument(() => { 21 | try { 22 | if (window.outerWidth && window.outerHeight) { 23 | return // nothing to do here 24 | } 25 | const windowFrame = 85 // probably OS and WM dependent 26 | window.outerWidth = window.innerWidth 27 | window.outerHeight = window.innerHeight + windowFrame 28 | } catch (err) {} 29 | }) 30 | } 31 | 32 | async beforeLaunch(options) { 33 | // Have viewport match window size, unless specified by user 34 | // https://github.com/GoogleChrome/puppeteer/issues/3688 35 | if (!('defaultViewport' in options)) { 36 | options.defaultViewport = null 37 | } 38 | return options 39 | } 40 | } 41 | 42 | module.exports = function(pluginConfig) { 43 | return new Plugin(pluginConfig) 44 | } 45 | -------------------------------------------------------------------------------- /undetected_playwright/puppeteer-extra-plugin-stealth/evasions/window.outerdimensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /undetected_playwright/tarnished.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Time : 2023/8/25 13:59 3 | # Author : QIN2DIM 4 | # GitHub : https://github.com/QIN2DIM 5 | # Description: 6 | from __future__ import annotations 7 | 8 | import inspect 9 | from pathlib import Path 10 | from typing import Dict, Any, Callable, Awaitable, List 11 | 12 | from playwright.async_api import BrowserContext as ASyncContext, async_playwright 13 | from playwright.sync_api import BrowserContext as SyncContext, sync_playwright 14 | 15 | enabled_evasions = [ 16 | "chrome.app", 17 | "chrome.csi", 18 | "chrome.loadTimes", 19 | "chrome.runtime", 20 | "iframe.contentWindow", 21 | "media.codecs", 22 | "navigator.hardwareConcurrency", 23 | "navigator.languages", 24 | "navigator.permissions", 25 | "navigator.plugins", 26 | "navigator.webdriver", 27 | "sourceurl", 28 | "webgl.vendor", 29 | "window.outerdimensions", 30 | ] 31 | 32 | 33 | class Tarnished: 34 | def __init__( 35 | self, 36 | user_data_dir: Path, 37 | *, 38 | record_dir: Path | None = None, 39 | record_har_path: Path | None = None, 40 | state_path: Path | None = None, 41 | ): 42 | self._user_data_dir = user_data_dir 43 | self._record_dir = record_dir 44 | self._record_har_path = record_har_path 45 | self.state_path = state_path 46 | 47 | @staticmethod 48 | def apply_stealth(context: SyncContext): 49 | for e in enabled_evasions: 50 | evasion_code = ( 51 | Path(__file__) 52 | .parent.joinpath(f"puppeteer-extra-plugin-stealth/evasions/{e}/index.js") 53 | .read_text(encoding="utf8") 54 | ) 55 | context.add_init_script(evasion_code) 56 | 57 | return context 58 | 59 | def storage_state(self, context: SyncContext): 60 | if self.state_path: 61 | context.storage_state(path=self.state_path) 62 | 63 | def execute( 64 | self, 65 | sequence: List | Callable[..., None], 66 | *, 67 | parameters: Dict[str, Any] = None, 68 | headless: bool = False, 69 | locale: str = "en-US", 70 | **kwargs, 71 | ): 72 | with sync_playwright() as p: 73 | context = p.firefox.launch_persistent_context( 74 | user_data_dir=self._user_data_dir, 75 | headless=headless, 76 | locale=locale, 77 | record_video_dir=self._record_dir, 78 | record_har_path=self._record_har_path, 79 | args=["--hide-crash-restore-bubble"], 80 | **kwargs, 81 | ) 82 | self.apply_stealth(context) 83 | 84 | if not isinstance(sequence, list): 85 | sequence = [sequence] 86 | for container in sequence: 87 | kws = {} 88 | params = inspect.signature(container).parameters 89 | if parameters and isinstance(parameters, dict): 90 | for name in params: 91 | if name != "context" and name in parameters: 92 | kws[name] = parameters[name] 93 | if not kws: 94 | container(context) 95 | else: 96 | container(context, **kws) 97 | context.close() 98 | 99 | 100 | class Malenia: 101 | def __init__( 102 | self, 103 | user_data_dir: Path, 104 | *, 105 | record_dir: Path | None = None, 106 | record_har_path: Path | None = None, 107 | state_path: Path | None = None, 108 | ): 109 | self._user_data_dir = user_data_dir 110 | self._record_dir = record_dir 111 | self._record_har_path = record_har_path 112 | self.state_path = state_path 113 | 114 | @staticmethod 115 | async def apply_stealth(context: ASyncContext): 116 | for e in enabled_evasions: 117 | evasion_code = ( 118 | Path(__file__) 119 | .parent.joinpath(f"puppeteer-extra-plugin-stealth/evasions/{e}/index.js") 120 | .read_text(encoding="utf8") 121 | ) 122 | await context.add_init_script(evasion_code) 123 | 124 | return context 125 | 126 | async def storage_state(self, context: ASyncContext): 127 | if self.state_path: 128 | await context.storage_state(path=self.state_path) 129 | 130 | async def execute( 131 | self, 132 | sequence: Callable[..., Awaitable[...]] | List, 133 | *, 134 | parameters: Dict[str, Any] = None, 135 | headless: bool = False, 136 | locale: str = "en-US", 137 | **kwargs, 138 | ): 139 | async with async_playwright() as p: 140 | context = await p.firefox.launch_persistent_context( 141 | user_data_dir=self._user_data_dir, 142 | headless=headless, 143 | locale=locale, 144 | record_video_dir=self._record_dir, 145 | record_har_path=self._record_har_path, 146 | args=["--hide-crash-restore-bubble"], 147 | **kwargs, 148 | ) 149 | await self.apply_stealth(context) 150 | 151 | if not isinstance(sequence, list): 152 | sequence = [sequence] 153 | for container in sequence: 154 | kws = {} 155 | params = inspect.signature(container).parameters 156 | if parameters and isinstance(parameters, dict): 157 | for name in params: 158 | if name != "context" and name in parameters: 159 | kws[name] = parameters[name] 160 | if not kws: 161 | await container(context) 162 | else: 163 | await container(context, **kws) 164 | await context.close() 165 | --------------------------------------------------------------------------------