├── .gitignore ├── README.md ├── package.json ├── patches ├── playwright-core │ ├── lib.patch │ └── src.patch └── puppeteer-core │ ├── lib.patch │ └── src.patch └── scripts ├── patcher.js └── utils └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | yarn-error.log* 4 | .DS_Store 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🪄 Patches for undetectable browser automation 2 | 3 | This repo contains patches to enhance popular web automation libraries. Specifically, it targets the [`puppeteer`](https://github.com/puppeteer/puppeteer) and [`playwright`](https://github.com/microsoft/playwright) packages. 4 | 5 | Some aspects of automation libraries or browser behavior cannot be adjusted through settings or command-line switches. Therefore, we fix these issues by patching the library's source code. While this approach is fragile and may break as the libraries' source code changes over time, the goal is to maintain this repo with community help to keep the patches up to date. 6 | 7 | ## Do I really need any patches? 8 | Out of the box Puppeteer and Playwright come with some significant leaks that are easy to detect. It doesn't matter how good your proxies, fingeprints, and behaviour scripts, if you don't have it patched, you're just a big red flag for any major website. 9 | 10 | 🕵️ You can easily test your automation setup for major modern detections with [rebrowser-bot-detector](https://bot-detector.rebrowser.net/) ([sources and details](https://github.com/rebrowser/rebrowser-bot-detector)) 11 | 12 | | Before the patches 👎 | After the patches 👍 | 13 | |--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------| 14 | | ![before](https://github.com/user-attachments/assets/6fc29650-4ea9-4d27-a152-0b7b40cd2b92) | ![after](https://github.com/user-attachments/assets/2ba0db25-c0db-4015-9c83-731a355cd2e9) | 15 | 16 | ## Is there an easy drop-in replacement? 17 | If you don't want to mess with the patches and all possible errors, there is a drop-in solution for you. These packages have simply applied rebrowser-patches on top of the original code, nothing more. 18 | 19 | Puppeteer: [rebrowser-puppeteer](https://www.npmjs.com/package/rebrowser-puppeteer) ([src](https://github.com/rebrowser/rebrowser-puppeteer)) and [rebrowser-puppeteer-core](https://www.npmjs.com/package/rebrowser-puppeteer-core) ([src](https://github.com/rebrowser/rebrowser-puppeteer-core)) 20 | 21 | Playwright (Node.js): [rebrowser-playwright](https://www.npmjs.com/package/rebrowser-playwright) ([src](https://github.com/rebrowser/rebrowser-playwright)) and [rebrowser-playwright-core](https://www.npmjs.com/package/rebrowser-playwright-core) ([src](https://github.com/rebrowser/rebrowser-playwright-core)) 22 | 23 | Playwright (Python): [rebrowser-playwright](https://pypi.org/project/rebrowser-playwright/) ([src](https://github.com/rebrowser/rebrowser-playwright-python)) 24 | 25 | The easiest way to start using it is to fix your `package.json` to use new packages but keep the old name as an alias. This way, you don't need to change any source code of your automation. Here is how to do that: 26 | 1. Open `package.json` and replace `"puppeteer": "^23.3.1"` and `"puppeteer-core": "^23.3.1"` with `"puppeteer": "npm:rebrowser-puppeteer@^23.3.1"` and `"puppeteer-core": "npm:rebrowser-puppeteer-core@^23.3.1"`. Note: 23.3.1 is just an example, check the latest version on [npm](https://www.npmjs.com/package/rebrowser-puppeteer-core). 27 | 2. Run `npm install` (or `yarn install`) 28 | 29 | Another way is to actually use new packages instead of the original one. Here are the steps you need to follow: 30 | 1. Open `package.json` and replace `puppeteer` and `puppeteer-core` packages with `rebrowser-puppeteer` and `rebrowser-puppeteer-core`. Don't change versions of the packages, just replace the names. 31 | 2. Run `npm install` (or `yarn install`) 32 | 3. Find and replace in your scripts any mentions of `puppeteer` and `puppeteer-core` with `rebrowser-puppeteer` and `rebrowser-puppeteer-core` 33 | 34 | 🚀 That's it! Just visit the [rebrowser-bot-detector](https://bot-detector.rebrowser.net/) page and test your patched browser. 35 | 36 | Our goal is to maintain and support these drop-in replacement packages with the latest versions, but we mainly focus on fresh versions, so if you're still using puppeteer 13.3.7 from the early 90s, it might be a good time to upgrade. There's a high chance that it won't really break anything as the API is quite stable over time. 37 | 38 | ## Available patches 39 | ### Fix `Runtime.Enable` leak 40 | Popular automation libraries rely on the CDP command `Runtime.Enable`, which allows receiving events from the `Runtime.` domain. This is crucial for managing execution contexts used to evaluate JavaScript on pages, a key feature for any automation process. 41 | 42 | However, there's a technique that detects the usage of this command, revealing that the browser is controlled by automation software like Puppeteer or Playwright. This technique is **used by all major anti-bot software** such as Cloudflare, DataDome, and others. 43 | 44 | > We've prepared a full article about our investigation on this leak, which you can read in [our blog](https://rebrowser.net/blog/how-to-fix-runtime-enable-cdp-detection-of-puppeteer-playwright-and-other-automation-libraries-61740). 45 | 46 | For more details on this technique, read DataDome's blog post: [How New Headless Chrome & the CDP Signal Are Impacting Bot Detection](https://datadome.co/threat-research/how-new-headless-chrome-the-cdp-signal-are-impacting-bot-detection/). 47 | In brief, it's a few lines of JavaScript on the page that are automatically called if `Runtime.Enable` was used. 48 | 49 | Our fix disables the automatic `Runtime.Enable` command on every frame. Instead, we manually create contexts with unknown IDs when a frame is created. Then, when code needs to be executed, there are multiple ways to get the context ID. 50 | 51 | #### 1. Create a new binding in the main world, call it and save its context ID. 52 | 🟢 Pros: The ultimate approach that keeps access to the main world and works with web workers and iframes. You don't need to change any of your existing codebase. 53 | 54 | 🔴 Cons: None are discovered so far. 55 | 56 | #### 2. Create a new isolated context via `Page.createIsolatedWorld` and save its ID. 57 | 🟢 Pros: All your code will be executed in a separate isolated world, preventing page scripts from detecting your changes via MutationObserver and other techniques. 58 | 59 | 🔴 Cons: You won't be able to access main context variables and code. While this is necessary for some use cases, the isolated context generally works fine for most scenarios. Also, web workers don't allow creating new worlds, so you can't execute your code inside a worker. This is a niche use case but may matter in some situations. There is a workaround for this issue, please read [How to Access Main Context Objects from Isolated Context in Puppeteer & Playwright](https://rebrowser.net/blog/how-to-access-main-context-objects-from-isolated-context-in-puppeteer-and-playwright-23741). 60 | 61 | #### 3. Call `Runtime.Enable` and then immediately call `Runtime.Disable`. 62 | This triggers `Runtime.executionContextCreated` events, allowing us to catch the proper context ID. 63 | 64 | 🟢 Pros: You will have full access to the main context. 65 | 66 | 🔴 Cons: There's a slight chance that during this short timeframe, the page will call code that leads to the leak. The risk is low, as detection code is usually called during specific actions like CAPTCHA pages or login/registration forms, typically right after the page loads. Your business logic is usually called a bit later. 67 | 68 | > 🎉 Our tests show that all these approaches are currently undetectable by Cloudflare or DataDome. 69 | 70 | Note: you can change settings for this patch on the fly using an environment variable. This allows you to easily switch between patched and non-patched versions based on your business logic. 71 | 72 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=addBinding` — addBinding technique (default) 73 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=alwaysIsolated` — always run all scripts in isolated context 74 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=enableDisable` — use Enable/Disable technique 75 | - `REBROWSER_PATCHES_RUNTIME_FIX_MODE=0` — completely disable fix for this leak 76 | - `REBROWSER_PATCHES_DEBUG=1` — enable some debugging messages 77 | 78 | Remember, you can set these variables in different ways, for example, in code: 79 | ```js 80 | process.env.REBROWSER_PATCHES_RUNTIME_FIX_MODE = "alwaysIsolated" 81 | ``` 82 | or in command line: 83 | ```shell 84 | REBROWSER_PATCHES_RUNTIME_FIX_MODE=alwaysIsolated node app.js 85 | ``` 86 | 87 | ### Change sourceURL to generic script name 88 | By default, Puppeteer adds `//# sourceURL=pptr:...` to every script in `page.evaluate()`. A remote website can detect this behavior and raise red flags. 89 | This patch changes it to `//# sourceURL=app.js`. You can also adjust it via environment variable: 90 | ```shell 91 | # use any generic filename 92 | REBROWSER_PATCHES_SOURCE_URL=jquery.min.js 93 | # use 0 to completely disable this patch 94 | REBROWSER_PATCHES_SOURCE_URL=0 95 | ``` 96 | 97 | ### Method to access browser CDP connection 98 | Sometimes, it could be very useful to access a CDP session at a browser level. For example, when you want to implement some custom CDP command. There is a method `page._client()` that returns CDP session for the current page instance, but there is no such method for browser instance. 99 | This patch adds a new method `_connection()` to Browser class, so you can use it like this: 100 | ```js 101 | browser._connection().on('Rebrowser.addRunEvent', (params) => { ... }) 102 | ``` 103 | *Note: it's not detectable by external website scripts, it's just for your convenience.* 104 | 105 | ### Change default utility world name 106 | The default utility world name is `'__puppeteer_utility_world__' + packageVersion`. Sometimes you might want to change it to something else. This patch changes it to `util` and allows you to customize it via env variable: 107 | ```shell 108 | REBROWSER_PATCHES_UTILITY_WORLD_NAME=customUtilityWorld 109 | # use 0 to completely disable this patch 110 | REBROWSER_PATCHES_UTILITY_WORLD_NAME=0 111 | ``` 112 | This env variable cannot be changed on the fly, you have to set it before running your script because it's used at the moment when the module is getting imported. 113 | 114 | | Before patch 👎 | After patch 👍 | 115 | |--------| --- | 116 | | ![before](https://github.com/user-attachments/assets/3f6719e8-37ab-4451-be19-f854d66184d0) | ![after](https://github.com/user-attachments/assets/5425ab0e-50bc-4c40-b94f-443011fdb210) | 117 | 118 | 119 | *Note: it's not detectable by external website scripts, but Google might use this information in their proprietary Chrome; we never know.* 120 | 121 | ## Usage 122 | This package is designed to be run against an installed library. Install the library, then call the patcher, and it's ready to go. 123 | 124 | In the root folder of your project, run: 125 | ``` 126 | npx rebrowser-patches@latest patch --packageName puppeteer-core 127 | ``` 128 | 129 | You can easily revert all changes with this command: 130 | ``` 131 | npx rebrowser-patches@latest unpatch --packageName puppeteer-core 132 | ``` 133 | 134 | You can also patch a package by providing the full path to its folder, for example: 135 | 136 | ``` 137 | npx rebrowser-patches@latest patch --packagePath /web/app/node_modules/puppeteer-core-custom 138 | ``` 139 | 140 | You can see all command-line options by running `npx rebrowser-patches@latest --help`, but currently, there's just one patch for one library, so you don't need to configure anything. 141 | 142 | ⚠️ Be aware that after running `npm install` or `yarn install` in your project folder, it might override all the changes from the patches. You'll need to run the patcher again to keep the patches in place. 143 | 144 | ## How to update the patches? 145 | If you already have your package patched and want to update to the latest version of rebrowser-patches, the easiest way would be to delete `node_modules/puppeteer-core`, then run `npm install` or `yarn install --check-files`, and then run `npx rebrowser-patches@latest patch`. 146 | 147 | ## How to patch Java/Python/.NET versions of Playwright? 148 | All these versions are just wrappers around Node.js version of Playwright. You need to find `driver` folder inside your Playwright package and run this patch with `--packagePath=$yourDriverFolder/$yourPlatform/package`. 149 | 150 | ## Puppeteer support 151 | ✅ Latest fully tested version: 24.8.1 (released 2025-05-06) 152 | 153 | ## Playwright support 154 | Playwright patches include: 155 | - `Runtime.enable` leak: `addBinding` and `alwaysIsolated`modes. 156 | - Ability to change utility world name via `REBROWSER_PATCHES_UTILITY_WORLD_NAME` env variable. 157 | - More patches are coming, star and follow the repo. 158 | 159 | Important: `page.pause()` method doesn't work with the enabled fix, it needs more investigation. You can just disable the fix completely while debugging using `REBROWSER_PATCHES_RUNTIME_FIX_MODE=0` env variable. 160 | 161 | These patches work only for Chrome for now. If you really want to use it with WebKit or Firefox, please open a new issue. 162 | 163 | ✅ Latest fully tested version: 1.52.0 (released 2025-04-17) 164 | 165 | ## How to use `rebrowser-puppeteer` with `puppeteer-extra`? 166 | Use `addExtra` method, here is the example: 167 | ``` 168 | // before 169 | import puppeteer from 'puppeteer-extra' 170 | 171 | // after 172 | import { addExtra } from 'puppeteer-extra' 173 | import rebrowserPuppeteer from 'rebrowser-puppeteer-core' 174 | const puppeteer = addExtra(rebrowserPuppeteer) 175 | ``` 176 | 177 | ## Follow the project 178 | We're currently developing more patches to improve web automation transparency, which will be released in this repo soon. Please support the project by clicking ⭐️ star or watch button. 179 | 180 | 💭 If you have any ideas, thoughts, or questions, feel free to reach out to our team by [email](mailto:info@rebrowser.net) or use the [issues section](https://github.com/rebrowser/rebrowser-patches/issues). 181 | 182 | ## The fix doesn't help, I'm still getting blocked 🤯 183 | ⚠️ It's important to know that this fix alone won't make your browser bulletproof and undetectable. You need to address **many other aspects** such as proxies, proper user-agent and fingerprints (canvas, WebGL), and more. 184 | 185 | Always keep in mind: the less you manipulate browser internals via JS injections, the better. There are ways to detect that internal objects such as console, navigator, and others were affected by Proxy objects or Object.defineProperty. It's tricky, but it's always a cat-and-mouse game. 186 | 187 | If you've tried everything and still face issues, try asking a question in the issues section or consider using cloud solutions from Rebrowser. 188 | 189 | ## What is Rebrowser? 190 | This package is sponsored and maintained by [Rebrowser](https://rebrowser.net). We allow you to scale your browser automation and web scraping in the cloud with hundreds of unique fingerprints. 191 | 192 | Our cloud browsers have great success rates and come with nice features such as notifications if your library uses `Runtime.Enable` during execution or has other red flags that could be improved. [Create an account](https://rebrowser.net) today to get invited to test our bleeding-edge platform and take your automation business to the next level. 193 | 194 | [![Automated warnings](https://github.com/user-attachments/assets/5bee67ed-2ddd-4d80-9404-f65f19a865ec)](https://rebrowser.net/docs/sensitive-cdp-methods) 195 | 196 | ## Patch command on Windows 197 | When you try to run this patcher on a Windows machine, you will probably encounter an error because the patch command is not found. To fix this, you need to install [Git](https://git-scm.com/download/win), which includes patch.exe. After you have installed it, you need to add it to your PATH: 198 | 199 | ``` 200 | set PATH=%PATH%;C:\Program Files\Git\usr\bin\ 201 | ``` 202 | 203 | You can check that patch.exe is installed correctly by using next command: 204 | ``` 205 | patch -v 206 | ``` 207 | 208 | ### Special thanks 209 | [zfcsoftware/puppeteer-real-browser](https://github.com/zfcsoftware/puppeteer-real-browser) - general ideas and contribution to the automation community 210 | 211 | [Kaliiiiiiiiii-Vinyzu/patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) - set of patches to fix Playwright leaks 212 | 213 | [kaliiiiiiiiii/brotector](https://github.com/kaliiiiiiiiii/brotector) - some modern tests, algorithm to distinguish CDP vs devtools 214 | 215 | [prescience-data/harden-puppeteer](https://github.com/prescience-data/harden-puppeteer) - one of the pioneers of the execution in an isolated world 216 | 217 | [puppeteer-extra-plugin-stealth](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth) - where it all started, big props to all the contributors and the community 🙏 berstend and co are the goats 218 | 219 | 220 | ### Disclaimer 221 | 222 | No responsibility is accepted for the use of this software. This software is intended for educational and informational purposes only. Users should use this software at their own risk. The developers of the software cannot be held liable for any damages that may result from the use of this software. This software is not intended to bypass any security measures, including but not limited to CAPTCHAs, anti-bot systems, or other protective mechanisms employed by websites. The software must not be used for malicious purposes. By using this software, you agree to this disclaimer and acknowledge that you are using the software responsibly and in compliance with all applicable laws and regulations. 223 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rebrowser-patches", 3 | "version": "1.0.19", 4 | "description": "Collection of patches for puppeteer and playwright to avoid automation detection and leaks. Helps to avoid Cloudflare and DataDome CAPTCHA pages. Easy to patch/unpatch, can be enabled/disabled on demand.", 5 | "keywords": [ 6 | "automation", 7 | "bot", 8 | "bot-detection", 9 | "crawler", 10 | "crawling", 11 | "chromedriver", 12 | "webdriver", 13 | "headless", 14 | "headless-chrome", 15 | "stealth", 16 | "captcha", 17 | "scraping", 18 | "web-scraping", 19 | "cloudflare", 20 | "datadome", 21 | "chrome", 22 | "puppeteer", 23 | "puppeteer-extra", 24 | "selenium", 25 | "playwright", 26 | "rebrowser", 27 | "rebrowser-patches" 28 | ], 29 | "author": { 30 | "name": "Rebrowser", 31 | "email": "info@rebrowser.net", 32 | "url": "https://rebrowser.net" 33 | }, 34 | "contributors": [ 35 | "Nick Webson " 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/rebrowser/rebrowser-patches" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/rebrowser/rebrowser-patches/issues" 43 | }, 44 | "files": [ 45 | "/patches", 46 | "/scripts" 47 | ], 48 | "homepage": "https://rebrowser.net", 49 | "main": "scripts/patcher.js", 50 | "bin": "./scripts/patcher.js", 51 | "license": "MIT", 52 | "type": "module", 53 | "scripts": { 54 | "start": "node ./scripts/patcher.js" 55 | }, 56 | "dependencies": { 57 | "yargs": "^17.7.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /patches/playwright-core/lib.patch: -------------------------------------------------------------------------------- 1 | --- a/lib/server/chromium/crConnection.js 2 | +++ b/lib/server/chromium/crConnection.js 3 | @@ -159,6 +159,127 @@ 4 | } 5 | this._callbacks.clear(); 6 | } 7 | + async __re__emitExecutionContext({ 8 | + world, 9 | + targetId, 10 | + frame = null 11 | + }) { 12 | + const fixMode = process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] || "addBinding"; 13 | + const utilityWorldName = process.env["REBROWSER_PATCHES_UTILITY_WORLD_NAME"] !== "0" ? process.env["REBROWSER_PATCHES_UTILITY_WORLD_NAME"] || "util" : "__playwright_utility_world__"; 14 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][crSession] targetId = ${targetId}, world = ${world}, frame = ${frame ? "Y" : "N"}, fixMode = ${fixMode}`); 15 | + let getWorldPromise; 16 | + if (fixMode === "addBinding") { 17 | + if (world === "utility") { 18 | + getWorldPromise = this.__re__getIsolatedWorld({ 19 | + client: this, 20 | + frameId: targetId, 21 | + worldName: utilityWorldName 22 | + }).then((contextId) => { 23 | + return { 24 | + id: contextId, 25 | + // use UTILITY_WORLD_NAME value from crPage.ts otherwise _onExecutionContextCreated will ignore it 26 | + name: "__playwright_utility_world__", 27 | + auxData: { 28 | + frameId: targetId, 29 | + isDefault: false 30 | + } 31 | + }; 32 | + }); 33 | + } else if (world === "main") { 34 | + getWorldPromise = this.__re__getMainWorld({ 35 | + client: this, 36 | + frameId: targetId, 37 | + isWorker: frame === null 38 | + }).then((contextId) => { 39 | + return { 40 | + id: contextId, 41 | + name: "", 42 | + auxData: { 43 | + frameId: targetId, 44 | + isDefault: true 45 | + } 46 | + }; 47 | + }); 48 | + } 49 | + } else if (fixMode === "alwaysIsolated") { 50 | + getWorldPromise = this.__re__getIsolatedWorld({ 51 | + client: this, 52 | + frameId: targetId, 53 | + worldName: utilityWorldName 54 | + }).then((contextId) => { 55 | + return { 56 | + id: contextId, 57 | + name: "", 58 | + auxData: { 59 | + frameId: targetId, 60 | + isDefault: true 61 | + } 62 | + }; 63 | + }); 64 | + } 65 | + const contextPayload = await getWorldPromise; 66 | + this.emit("Runtime.executionContextCreated", { 67 | + context: contextPayload 68 | + }); 69 | + } 70 | + async __re__getMainWorld({ client, frameId, isWorker = false }) { 71 | + let contextId; 72 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join(""); 73 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`); 74 | + await client.send("Runtime.addBinding", { 75 | + name: randomName 76 | + }); 77 | + const bindingCalledHandler = ({ name, payload, executionContextId }) => { 78 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log("[rebrowser-patches][bindingCalledHandler]", { 79 | + name, 80 | + payload, 81 | + executionContextId 82 | + }); 83 | + if (contextId > 0) { 84 | + return; 85 | + } 86 | + if (name !== randomName) { 87 | + return; 88 | + } 89 | + if (payload !== frameId) { 90 | + return; 91 | + } 92 | + contextId = executionContextId; 93 | + client.off("Runtime.bindingCalled", bindingCalledHandler); 94 | + }; 95 | + client.on("Runtime.bindingCalled", bindingCalledHandler); 96 | + if (isWorker) { 97 | + await client.send("Runtime.evaluate", { 98 | + expression: `this['${randomName}']('${frameId}')` 99 | + }); 100 | + } else { 101 | + await client.send("Page.addScriptToEvaluateOnNewDocument", { 102 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`, 103 | + runImmediately: true 104 | + }); 105 | + const createIsolatedWorldResult = await client.send("Page.createIsolatedWorld", { 106 | + frameId, 107 | + // use randomName for worldName to distinguish from normal utility world 108 | + worldName: randomName, 109 | + grantUniveralAccess: true 110 | + }); 111 | + await client.send("Runtime.evaluate", { 112 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`, 113 | + contextId: createIsolatedWorldResult.executionContextId 114 | + }); 115 | + } 116 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId }); 117 | + return contextId; 118 | + } 119 | + async __re__getIsolatedWorld({ client, frameId, worldName }) { 120 | + const createIsolatedWorldResult = await client.send("Page.createIsolatedWorld", { 121 | + frameId, 122 | + worldName, 123 | + grantUniveralAccess: true 124 | + }); 125 | + process.env["REBROWSER_PATCHES_DEBUG"] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult); 126 | + return createIsolatedWorldResult.executionContextId; 127 | + } 128 | } 129 | class CDPSession extends import_events.EventEmitter { 130 | constructor(parentSession, sessionId) { 131 | 132 | --- a/lib/server/chromium/crDevTools.js 133 | +++ b/lib/server/chromium/crDevTools.js 134 | @@ -72,7 +72,11 @@ 135 | }).catch((e) => null); 136 | }); 137 | Promise.all([ 138 | - session.send("Runtime.enable"), 139 | + (() => { 140 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") { 141 | + return session.send("Runtime.enable", {}); 142 | + } 143 | + })(), 144 | session.send("Runtime.addBinding", { name: kBindingName }), 145 | session.send("Page.enable"), 146 | session.send("Page.addScriptToEvaluateOnNewDocument", { source: ` 147 | 148 | --- a/lib/server/chromium/crPage.js 149 | +++ b/lib/server/chromium/crPage.js 150 | @@ -425,7 +425,11 @@ 151 | }), 152 | this._client.send("Log.enable", {}), 153 | lifecycleEventsEnabled = this._client.send("Page.setLifecycleEventsEnabled", { enabled: true }), 154 | - this._client.send("Runtime.enable", {}), 155 | + (() => { 156 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") { 157 | + return this._client.send("Runtime.enable", {}); 158 | + } 159 | + })(), 160 | this._client.send("Runtime.addBinding", { name: import_page.PageBinding.kPlaywrightBinding }), 161 | this._client.send("Page.addScriptToEvaluateOnNewDocument", { 162 | source: "", 163 | @@ -601,13 +605,15 @@ 164 | return; 165 | } 166 | const url = event.targetInfo.url; 167 | - const worker = new import_page2.Worker(this._page, url); 168 | + const worker = new import_page2.Worker(this._page, url, event.targetInfo.targetId, session); 169 | this._page._addWorker(event.sessionId, worker); 170 | this._workerSessions.set(event.sessionId, session); 171 | session.once("Runtime.executionContextCreated", async (event2) => { 172 | worker._createExecutionContext(new import_crExecutionContext.CRExecutionContext(session, event2.context)); 173 | }); 174 | - session._sendMayFail("Runtime.enable"); 175 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") { 176 | + session._sendMayFail("Runtime.enable"); 177 | + } 178 | this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? void 0).catch(() => { 179 | }); 180 | session._sendMayFail("Runtime.runIfWaitingForDebugger"); 181 | 182 | --- a/lib/server/chromium/crServiceWorker.js 183 | +++ b/lib/server/chromium/crServiceWorker.js 184 | @@ -59,8 +59,10 @@ 185 | ).catch(() => { 186 | }); 187 | } 188 | - session.send("Runtime.enable", {}).catch((e) => { 189 | - }); 190 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0") { 191 | + session.send("Runtime.enable", {}).catch((e) => { 192 | + }); 193 | + } 194 | session.send("Runtime.runIfWaitingForDebugger").catch((e) => { 195 | }); 196 | session.on("Inspector.targetReloadedAfterCrash", () => { 197 | 198 | --- a/lib/server/frames.js 199 | +++ b/lib/server/frames.js 200 | @@ -434,6 +434,8 @@ 201 | this._startNetworkIdleTimer(); 202 | this._page.mainFrame()._recalculateNetworkIdle(this); 203 | this._onLifecycleEvent("commit"); 204 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client; 205 | + crSession.emit("Runtime.executionContextsCleared"); 206 | } 207 | setPendingDocument(documentInfo) { 208 | this._pendingDocument = documentInfo; 209 | @@ -605,11 +607,29 @@ 210 | async frameElement() { 211 | return this._page._delegate.getFrameElement(this); 212 | } 213 | - _context(world) { 214 | - return this._contextData.get(world).contextPromise.then((contextOrDestroyedReason) => { 215 | - if (contextOrDestroyedReason instanceof js.ExecutionContext) 216 | - return contextOrDestroyedReason; 217 | - throw new Error(contextOrDestroyedReason.destroyedReason); 218 | + _context(world, useContextPromise = false) { 219 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] === "0" || this._contextData.get(world).context || useContextPromise) { 220 | + return this._contextData.get(world).contextPromise.then((contextOrDestroyedReason) => { 221 | + if (contextOrDestroyedReason instanceof js.ExecutionContext) 222 | + return contextOrDestroyedReason; 223 | + throw new Error(contextOrDestroyedReason.destroyedReason); 224 | + }); 225 | + } 226 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client; 227 | + return crSession.__re__emitExecutionContext({ 228 | + world, 229 | + targetId: this._id, 230 | + frame: this 231 | + }).then(() => { 232 | + return this._context(world, true); 233 | + }).catch((error) => { 234 | + if (error.message.includes("No frame for given id found")) { 235 | + return { 236 | + destroyedReason: "Frame was detached" 237 | + }; 238 | + } 239 | + import_debugLogger.debugLogger.log("error", error); 240 | + console.error("[rebrowser-patches][frames._context] cannot get world, error:", error); 241 | }); 242 | } 243 | _mainContext() { 244 | 245 | --- a/lib/server/page.js 246 | +++ b/lib/server/page.js 247 | @@ -624,7 +624,7 @@ 248 | } 249 | } 250 | class Worker extends import_instrumentation.SdkObject { 251 | - constructor(parent, url) { 252 | + constructor(parent, url, targetId, session) { 253 | super(parent, "worker"); 254 | this._existingExecutionContext = null; 255 | this.openScope = new import_utils.LongStandingScope(); 256 | @@ -632,6 +632,8 @@ 257 | this._executionContextCallback = () => { 258 | }; 259 | this._executionContextPromise = new Promise((x) => this._executionContextCallback = x); 260 | + this._targetId = targetId; 261 | + this._session = session; 262 | } 263 | static { 264 | this.Events = { 265 | @@ -652,11 +654,20 @@ 266 | this.emit(Worker.Events.Close, this); 267 | this.openScope.close(new Error("Worker closed")); 268 | } 269 | + async getExecutionContext() { 270 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] !== "0" && !this._existingExecutionContext) { 271 | + await this._session.__re__emitExecutionContext({ 272 | + world: "main", 273 | + targetId: this._targetId 274 | + }); 275 | + } 276 | + return this._executionContextPromise; 277 | + } 278 | async evaluateExpression(expression, isFunction, arg) { 279 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg); 280 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: true, isFunction }, arg); 281 | } 282 | async evaluateExpressionHandle(expression, isFunction, arg) { 283 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg); 284 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: false, isFunction }, arg); 285 | } 286 | } 287 | class PageBinding { 288 | @@ -675,6 +686,9 @@ 289 | this.internal = name.startsWith("__pw"); 290 | } 291 | static async dispatch(page, payload, context) { 292 | + if (process.env["REBROWSER_PATCHES_RUNTIME_FIX_MODE"] !== "0" && !payload.includes("{")) { 293 | + return; 294 | + } 295 | const { name, seq, serializedArgs } = JSON.parse(payload); 296 | try { 297 | (0, import_utils.assert)(context.world); 298 | -------------------------------------------------------------------------------- /patches/playwright-core/src.patch: -------------------------------------------------------------------------------- 1 | 2 | --- a/src/server/chromium/crConnection.ts 3 | +++ b/src/server/chromium/crConnection.ts 4 | @@ -201,6 +201,153 @@ 5 | } 6 | this._callbacks.clear(); 7 | } 8 | + 9 | + async __re__emitExecutionContext({ 10 | + world, 11 | + targetId, 12 | + frame = null 13 | + }) { 14 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding' 15 | + const utilityWorldName = process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') : '__playwright_utility_world__'; 16 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][crSession] targetId = ${targetId}, world = ${world}, frame = ${frame ? 'Y' : 'N'}, fixMode = ${fixMode}`) 17 | + 18 | + let getWorldPromise 19 | + if (fixMode === 'addBinding') { 20 | + if (world === 'utility') { 21 | + getWorldPromise = this.__re__getIsolatedWorld({ 22 | + client: this, 23 | + frameId: targetId, 24 | + worldName: utilityWorldName, 25 | + }) 26 | + .then((contextId) => { 27 | + return { 28 | + id: contextId, 29 | + // use UTILITY_WORLD_NAME value from crPage.ts otherwise _onExecutionContextCreated will ignore it 30 | + name: '__playwright_utility_world__', 31 | + auxData: { 32 | + frameId: targetId, 33 | + isDefault: false 34 | + } 35 | + } 36 | + }) 37 | + } else if (world === 'main') { 38 | + getWorldPromise = this.__re__getMainWorld({ 39 | + client: this, 40 | + frameId: targetId, 41 | + isWorker: frame === null 42 | + }) 43 | + .then((contextId) => { 44 | + return { 45 | + id: contextId, 46 | + name: '', 47 | + auxData: { 48 | + frameId: targetId, 49 | + isDefault: true 50 | + } 51 | + } 52 | + }) 53 | + } 54 | + } else if (fixMode === 'alwaysIsolated') { 55 | + // use only utility context 56 | + getWorldPromise = this.__re__getIsolatedWorld({ 57 | + client: this, 58 | + frameId: targetId, 59 | + worldName: utilityWorldName, 60 | + }) 61 | + .then((contextId) => { 62 | + // make it look as main world 63 | + return { 64 | + id: contextId, 65 | + name: '', 66 | + auxData: { 67 | + frameId: targetId, 68 | + isDefault: true 69 | + } 70 | + } 71 | + }) 72 | + } 73 | + 74 | + const contextPayload = await getWorldPromise 75 | + this.emit('Runtime.executionContextCreated', { 76 | + context: contextPayload 77 | + }) 78 | + } 79 | + async __re__getMainWorld({ client, frameId, isWorker = false }: any) { 80 | + let contextId: any; 81 | + 82 | + // random name to make it harder to detect for any 3rd party script by watching window object and events 83 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join(''); 84 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`); 85 | + 86 | + // add the binding 87 | + await client.send('Runtime.addBinding', { 88 | + name: randomName, 89 | + }); 90 | + 91 | + // listen for 'Runtime.bindingCalled' event 92 | + const bindingCalledHandler = ({ name, payload, executionContextId }: any) => { 93 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', { 94 | + name, 95 | + payload, 96 | + executionContextId 97 | + }); 98 | + if (contextId > 0) { 99 | + // already acquired the id 100 | + return; 101 | + } 102 | + if (name !== randomName) { 103 | + // ignore irrelevant bindings 104 | + return; 105 | + } 106 | + if (payload !== frameId) { 107 | + // ignore irrelevant frames 108 | + return; 109 | + } 110 | + contextId = executionContextId; 111 | + // remove this listener 112 | + client.off('Runtime.bindingCalled', bindingCalledHandler); 113 | + }; 114 | + client.on('Runtime.bindingCalled', bindingCalledHandler); 115 | + 116 | + if (isWorker) { 117 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate 118 | + await client.send('Runtime.evaluate', { 119 | + expression: `this['${randomName}']('${frameId}')`, 120 | + }); 121 | + } else { 122 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent 123 | + await client.send('Page.addScriptToEvaluateOnNewDocument', { 124 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`, 125 | + runImmediately: true, 126 | + }); 127 | + 128 | + // create new isolated world for this frame 129 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 130 | + frameId, 131 | + // use randomName for worldName to distinguish from normal utility world 132 | + worldName: randomName, 133 | + grantUniveralAccess: true, 134 | + }); 135 | + 136 | + // emit event in the specific frame from the isolated world 137 | + await client.send('Runtime.evaluate', { 138 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`, 139 | + contextId: createIsolatedWorldResult.executionContextId, 140 | + }); 141 | + } 142 | + 143 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId }); 144 | + return contextId; 145 | + } 146 | + async __re__getIsolatedWorld({ client, frameId, worldName }: any) { 147 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 148 | + frameId, 149 | + worldName, 150 | + grantUniveralAccess: true, 151 | + }); 152 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult); 153 | + return createIsolatedWorldResult.executionContextId; 154 | + } 155 | } 156 | 157 | export class CDPSession extends EventEmitter { 158 | 159 | --- a/src/server/chromium/crDevTools.ts 160 | +++ b/src/server/chromium/crDevTools.ts 161 | @@ -67,7 +67,11 @@ 162 | }).catch(e => null); 163 | }); 164 | Promise.all([ 165 | - session.send('Runtime.enable'), 166 | + (() => { 167 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 168 | + return session.send('Runtime.enable', {}) 169 | + } 170 | + })(), 171 | session.send('Runtime.addBinding', { name: kBindingName }), 172 | session.send('Page.enable'), 173 | session.send('Page.addScriptToEvaluateOnNewDocument', { source: ` 174 | 175 | --- a/src/server/chromium/crPage.ts 176 | +++ b/src/server/chromium/crPage.ts 177 | @@ -507,7 +507,11 @@ 178 | }), 179 | this._client.send('Log.enable', {}), 180 | lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), 181 | - this._client.send('Runtime.enable', {}), 182 | + (() => { 183 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 184 | + return this._client.send('Runtime.enable', {}) 185 | + } 186 | + })(), 187 | this._client.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }), 188 | this._client.send('Page.addScriptToEvaluateOnNewDocument', { 189 | source: '', 190 | @@ -719,14 +723,17 @@ 191 | } 192 | 193 | const url = event.targetInfo.url; 194 | - const worker = new Worker(this._page, url); 195 | + const worker = new Worker(this._page, url, event.targetInfo.targetId, session); 196 | this._page._addWorker(event.sessionId, worker); 197 | this._workerSessions.set(event.sessionId, session); 198 | session.once('Runtime.executionContextCreated', async event => { 199 | worker._createExecutionContext(new CRExecutionContext(session, event.context)); 200 | }); 201 | - // This might fail if the target is closed before we initialize. 202 | - session._sendMayFail('Runtime.enable'); 203 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 204 | + // This might fail if the target is closed before we initialize. 205 | + session._sendMayFail('Runtime.enable'); 206 | + } 207 | + 208 | // TODO: attribute workers to the right frame. 209 | this._crPage._networkManager.addSession(session, this._page._frameManager.frame(this._targetId) ?? undefined).catch(() => {}); 210 | session._sendMayFail('Runtime.runIfWaitingForDebugger'); 211 | 212 | --- a/src/server/chromium/crServiceWorker.ts 213 | +++ b/src/server/chromium/crServiceWorker.ts 214 | @@ -45,7 +45,9 @@ 215 | this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {}); 216 | } 217 | 218 | - session.send('Runtime.enable', {}).catch(e => { }); 219 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 220 | + session.send('Runtime.enable', {}).catch(e => { }); 221 | + } 222 | session.send('Runtime.runIfWaitingForDebugger').catch(e => { }); 223 | session.on('Inspector.targetReloadedAfterCrash', () => { 224 | // Resume service worker after restart. 225 | 226 | --- a/src/server/frames.ts 227 | +++ b/src/server/frames.ts 228 | @@ -540,6 +540,8 @@ 229 | this._startNetworkIdleTimer(); 230 | this._page.mainFrame()._recalculateNetworkIdle(this); 231 | this._onLifecycleEvent('commit'); 232 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client 233 | + crSession.emit('Runtime.executionContextsCleared') 234 | } 235 | 236 | setPendingDocument(documentInfo: DocumentInfo | undefined) { 237 | @@ -743,12 +745,34 @@ 238 | return this._page._delegate.getFrameElement(this); 239 | } 240 | 241 | - _context(world: types.World): Promise { 242 | - return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => { 243 | - if (contextOrDestroyedReason instanceof js.ExecutionContext) 244 | - return contextOrDestroyedReason; 245 | - throw new Error(contextOrDestroyedReason.destroyedReason); 246 | - }); 247 | + _context(world: types.World, useContextPromise = false): Promise { 248 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0' || this._contextData.get(world).context || useContextPromise) { 249 | + return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => { 250 | + if (contextOrDestroyedReason instanceof js.ExecutionContext) 251 | + return contextOrDestroyedReason; 252 | + throw new Error(contextOrDestroyedReason.destroyedReason); 253 | + }); 254 | + } 255 | + 256 | + const crSession = (this._page._delegate._sessions.get(this._id) || this._page._delegate._mainFrameSession)._client 257 | + return crSession.__re__emitExecutionContext({ 258 | + world, 259 | + targetId: this._id, 260 | + frame: this 261 | + }) 262 | + .then(() => { 263 | + return this._context(world, true) 264 | + }) 265 | + .catch(error => { 266 | + if (error.message.includes('No frame for given id found')) { 267 | + // ignore, frame is already gone 268 | + return { 269 | + destroyedReason: 'Frame was detached' 270 | + } 271 | + } 272 | + debugLogger.log('error', error) 273 | + console.error('[rebrowser-patches][frames._context] cannot get world, error:', error) 274 | + }) 275 | } 276 | 277 | _mainContext(): Promise { 278 | 279 | --- a/src/server/page.ts 280 | +++ b/src/server/page.ts 281 | @@ -823,12 +823,16 @@ 282 | private _executionContextCallback: (value: js.ExecutionContext) => void; 283 | _existingExecutionContext: js.ExecutionContext | null = null; 284 | readonly openScope = new LongStandingScope(); 285 | + private _targetId: string; 286 | + private _session: any; 287 | 288 | - constructor(parent: SdkObject, url: string) { 289 | + constructor(parent: SdkObject, url: string, targetId: string, session: any) { 290 | super(parent, 'worker'); 291 | this._url = url; 292 | this._executionContextCallback = () => {}; 293 | this._executionContextPromise = new Promise(x => this._executionContextCallback = x); 294 | + this._targetId = targetId 295 | + this._session = session 296 | } 297 | 298 | _createExecutionContext(delegate: js.ExecutionContextDelegate) { 299 | @@ -848,12 +852,23 @@ 300 | this.openScope.close(new Error('Worker closed')); 301 | } 302 | 303 | + async getExecutionContext() { 304 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0' && !this._existingExecutionContext) { 305 | + await this._session.__re__emitExecutionContext({ 306 | + world: 'main', 307 | + targetId: this._targetId, 308 | + }) 309 | + } 310 | + 311 | + return this._executionContextPromise 312 | + } 313 | + 314 | async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise { 315 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg); 316 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: true, isFunction }, arg); 317 | } 318 | 319 | async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise { 320 | - return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg); 321 | + return js.evaluateExpression(await this.getExecutionContext(), expression, { returnByValue: false, isFunction }, arg); 322 | } 323 | } 324 | 325 | @@ -875,6 +890,10 @@ 326 | } 327 | 328 | static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) { 329 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0' && !payload.includes('{')) { 330 | + // ignore as it's not a JSON but a string from addBinding method 331 | + return 332 | + } 333 | const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload; 334 | try { 335 | assert(context.world); 336 | -------------------------------------------------------------------------------- /patches/puppeteer-core/lib.patch: -------------------------------------------------------------------------------- 1 | --- a/lib/cjs/puppeteer/cdp/Browser.d.ts 2 | +++ b/lib/cjs/puppeteer/cdp/Browser.d.ts 3 | @@ -32,6 +32,7 @@ 4 | _disposeContext(contextId?: string): Promise; 5 | wsEndpoint(): string; 6 | newPage(): Promise; 7 | + _connection(): Connection; 8 | _createPageInContext(contextId?: string): Promise; 9 | installExtension(path: string): Promise; 10 | uninstallExtension(id: string): Promise; 11 | 12 | --- a/lib/cjs/puppeteer/cdp/Browser.js 13 | +++ b/lib/cjs/puppeteer/cdp/Browser.js 14 | @@ -175,6 +175,10 @@ 15 | async newPage() { 16 | return await this.#defaultContext.newPage(); 17 | } 18 | + // rebrowser-patches: expose browser CDP session 19 | + _connection() { 20 | + return this.#connection; 21 | + } 22 | async _createPageInContext(contextId) { 23 | const { targetId } = await this.#connection.send('Target.createTarget', { 24 | url: 'about:blank', 25 | 26 | --- a/lib/cjs/puppeteer/cdp/ExecutionContext.d.ts 27 | +++ b/lib/cjs/puppeteer/cdp/ExecutionContext.d.ts 28 | @@ -22,6 +22,7 @@ 29 | bindingcalled: Protocol.Runtime.BindingCalledEvent; 30 | }> implements Disposable { 31 | #private; 32 | + _frameId: any; 33 | constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: IsolatedWorld); 34 | get id(): number; 35 | get puppeteerUtil(): Promise>; 36 | @@ -116,6 +117,10 @@ 37 | * {@link ElementHandle | element handle}. 38 | */ 39 | evaluateHandle = EvaluateFunc>(pageFunction: Func | string, ...args: Params): Promise>>>; 40 | + clear(newId: any): void; 41 | + __re__getMainWorld({ client, frameId, isWorker }: any): Promise; 42 | + __re__getIsolatedWorld({ client, frameId, worldName }: any): Promise; 43 | + acquireContextId(tryCount?: number): Promise; 44 | [disposeSymbol](): void; 45 | } 46 | //# sourceMappingURL=ExecutionContext.d.ts.map 47 | \ No newline at end of file 48 | 49 | --- a/lib/cjs/puppeteer/cdp/ExecutionContext.js 50 | +++ b/lib/cjs/puppeteer/cdp/ExecutionContext.js 51 | @@ -86,6 +86,7 @@ 52 | #client; 53 | #world; 54 | #id; 55 | + _frameId; 56 | #name; 57 | #disposables = new disposable_js_1.DisposableStack(); 58 | constructor(client, contextPayload, world) { 59 | @@ -96,16 +97,22 @@ 60 | if (contextPayload.name) { 61 | this.#name = contextPayload.name; 62 | } 63 | + // rebrowser-patches: keep frameId to use later 64 | + if (contextPayload.auxData?.frameId) { 65 | + this._frameId = contextPayload.auxData?.frameId; 66 | + } 67 | const clientEmitter = this.#disposables.use(new EventEmitter_js_1.EventEmitter(this.#client)); 68 | clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this)); 69 | - clientEmitter.on('Runtime.executionContextDestroyed', async (event) => { 70 | - if (event.executionContextId === this.#id) { 71 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 72 | + clientEmitter.on('Runtime.executionContextDestroyed', async (event) => { 73 | + if (event.executionContextId === this.#id) { 74 | + this[disposable_js_1.disposeSymbol](); 75 | + } 76 | + }); 77 | + clientEmitter.on('Runtime.executionContextsCleared', async () => { 78 | this[disposable_js_1.disposeSymbol](); 79 | - } 80 | - }); 81 | - clientEmitter.on('Runtime.executionContextsCleared', async () => { 82 | - this[disposable_js_1.disposeSymbol](); 83 | - }); 84 | + }); 85 | + } 86 | clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this)); 87 | clientEmitter.on(CDPSession_js_1.CDPSessionEvent.Disconnected, () => { 88 | this[disposable_js_1.disposeSymbol](); 89 | @@ -328,7 +335,181 @@ 90 | async evaluateHandle(pageFunction, ...args) { 91 | return await this.#evaluate(false, pageFunction, ...args); 92 | } 93 | + // rebrowser-patches: alternative to dispose 94 | + clear(newId) { 95 | + this.#id = newId; 96 | + this.#bindings = new Map(); 97 | + this.#bindingsInstalled = false; 98 | + this.#puppeteerUtil = undefined; 99 | + } 100 | + async __re__getMainWorld({ client, frameId, isWorker = false }) { 101 | + let contextId; 102 | + // random name to make it harder to detect for any 3rd party script by watching window object and events 103 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join(''); 104 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`); 105 | + // add the binding 106 | + await client.send('Runtime.addBinding', { 107 | + name: randomName, 108 | + }); 109 | + // listen for 'Runtime.bindingCalled' event 110 | + const bindingCalledHandler = ({ name, payload, executionContextId }) => { 111 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', { 112 | + name, 113 | + payload, 114 | + executionContextId 115 | + }); 116 | + if (contextId > 0) { 117 | + // already acquired the id 118 | + return; 119 | + } 120 | + if (name !== randomName) { 121 | + // ignore irrelevant bindings 122 | + return; 123 | + } 124 | + if (payload !== frameId) { 125 | + // ignore irrelevant frames 126 | + return; 127 | + } 128 | + contextId = executionContextId; 129 | + // remove this listener 130 | + client.off('Runtime.bindingCalled', bindingCalledHandler); 131 | + }; 132 | + client.on('Runtime.bindingCalled', bindingCalledHandler); 133 | + if (isWorker) { 134 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate 135 | + await client.send('Runtime.evaluate', { 136 | + expression: `this['${randomName}']('${frameId}')`, 137 | + }); 138 | + } 139 | + else { 140 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent 141 | + await client.send('Page.addScriptToEvaluateOnNewDocument', { 142 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`, 143 | + runImmediately: true, 144 | + }); 145 | + // create new isolated world for this frame 146 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 147 | + frameId, 148 | + // use randomName for worldName to distinguish from normal utility world 149 | + worldName: randomName, 150 | + grantUniveralAccess: true, 151 | + }); 152 | + // emit event in the specific frame from the isolated world 153 | + await client.send('Runtime.evaluate', { 154 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`, 155 | + contextId: createIsolatedWorldResult.executionContextId, 156 | + }); 157 | + } 158 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId }); 159 | + return contextId; 160 | + } 161 | + async __re__getIsolatedWorld({ client, frameId, worldName }) { 162 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 163 | + frameId, 164 | + worldName, 165 | + grantUniveralAccess: true, 166 | + }); 167 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult); 168 | + return createIsolatedWorldResult.executionContextId; 169 | + } 170 | + // rebrowser-patches: get context id if it's missing 171 | + async acquireContextId(tryCount = 1) { 172 | + if (this.#id > 0) { 173 | + return; 174 | + } 175 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 176 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${this.#id}, name = ${this.#name}, fixMode = ${fixMode}, tryCount = ${tryCount}`); 177 | + let contextId; 178 | + let tryAgain = true; 179 | + let errorMessage = 'N/A'; 180 | + if (fixMode === 'addBinding') { 181 | + try { 182 | + if (this.#id === -2) { 183 | + // isolated world 184 | + contextId = await this.__re__getIsolatedWorld({ 185 | + client: this.#client, 186 | + frameId: this._frameId, 187 | + worldName: this.#name, 188 | + }); 189 | + } 190 | + else { 191 | + // main world 192 | + contextId = await this.__re__getMainWorld({ 193 | + client: this.#client, 194 | + frameId: this._frameId, 195 | + isWorker: this.#id === -3, 196 | + }); 197 | + } 198 | + } 199 | + catch (error) { 200 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error); 201 | + errorMessage = error.message; 202 | + if (error instanceof Error) { 203 | + if (error.message.includes('No frame for given id found') || 204 | + error.message.includes('Target closed') || 205 | + error.message.includes('Session closed')) { 206 | + // target doesn't exist anymore, don't try again 207 | + tryAgain = false; 208 | + } 209 | + } 210 | + (0, util_js_1.debugError)(error); 211 | + } 212 | + } 213 | + else if (fixMode === 'alwaysIsolated') { 214 | + if (this.#id === -3) { 215 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode'); 216 | + } 217 | + contextId = await this.__re__getIsolatedWorld({ 218 | + client: this.#client, 219 | + frameId: this._frameId, 220 | + worldName: this.#name, 221 | + }); 222 | + } 223 | + else if (fixMode === 'enableDisable') { 224 | + const executionContextCreatedHandler = ({ context }) => { 225 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${this.#id}, name = ${this.#name}, contextId = ${contextId}, event.context.id = ${context.id}`); 226 | + if (contextId > 0) { 227 | + // already acquired the id 228 | + return; 229 | + } 230 | + if (this.#id === -1) { 231 | + // main world 232 | + if (context.auxData && context.auxData['isDefault']) { 233 | + contextId = context.id; 234 | + } 235 | + } 236 | + else if (this.#id === -2) { 237 | + // utility world 238 | + if (this.#name === context.name) { 239 | + contextId = context.id; 240 | + } 241 | + } 242 | + else if (this.#id === -3) { 243 | + // web worker 244 | + contextId = context.id; 245 | + } 246 | + }; 247 | + this.#client.on('Runtime.executionContextCreated', executionContextCreatedHandler); 248 | + await this.#client.send('Runtime.enable'); 249 | + await this.#client.send('Runtime.disable'); 250 | + this.#client.off('Runtime.executionContextCreated', executionContextCreatedHandler); 251 | + } 252 | + if (!contextId) { 253 | + if (!tryAgain || tryCount >= 3) { 254 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`); 255 | + } 256 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`); 257 | + return this.acquireContextId(tryCount + 1); 258 | + } 259 | + this.#id = contextId; 260 | + } 261 | async #evaluate(returnByValue, pageFunction, ...args) { 262 | + // rebrowser-patches: context id is missing, acquire it and try again 263 | + if (this.#id < 0) { 264 | + await this.acquireContextId(); 265 | + // @ts-ignore 266 | + return this.#evaluate(returnByValue, pageFunction, ...args); 267 | + } 268 | const sourceUrlComment = (0, util_js_1.getSourceUrlComment)((0, util_js_1.getSourcePuppeteerURLIfAvailable)(pageFunction)?.toString() ?? 269 | util_js_1.PuppeteerURL.INTERNAL_URL); 270 | if ((0, util_js_1.isString)(pageFunction)) { 271 | 272 | --- a/lib/cjs/puppeteer/cdp/FrameManager.js 273 | +++ b/lib/cjs/puppeteer/cdp/FrameManager.js 274 | @@ -154,6 +154,10 @@ 275 | this.#onFrameStoppedLoading(event.frameId); 276 | }); 277 | session.on('Runtime.executionContextCreated', async (event) => { 278 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 279 | + // rebrowser-patches: ignore default logic 280 | + return; 281 | + } 282 | await this.#frameTreeHandled?.valueOrThrow(); 283 | this.#onExecutionContextCreated(event.context, session); 284 | }); 285 | @@ -178,9 +182,16 @@ 286 | this.#frameTreeHandled?.resolve(); 287 | }), 288 | client.send('Page.setLifecycleEventsEnabled', { enabled: true }), 289 | - client.send('Runtime.enable').then(() => { 290 | - return this.#createIsolatedWorld(client, util_js_1.UTILITY_WORLD_NAME); 291 | - }), 292 | + (() => { 293 | + // rebrowser-patches: skip Runtime.enable 294 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 295 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize'); 296 | + return this.#createIsolatedWorld(client, util_js_1.UTILITY_WORLD_NAME); 297 | + } 298 | + return client.send('Runtime.enable').then(() => { 299 | + return this.#createIsolatedWorld(client, util_js_1.UTILITY_WORLD_NAME); 300 | + }); 301 | + })(), 302 | ...(frame 303 | ? Array.from(this.#scriptsToEvaluateOnNewDocument.values()) 304 | : []).map(script => { 305 | @@ -190,6 +201,26 @@ 306 | return frame?.addExposedFunctionBinding(binding); 307 | }), 308 | ]); 309 | + // rebrowser-patches: manually create main world context 310 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 311 | + this.frames() 312 | + .filter(frame => { 313 | + return frame.client === client; 314 | + }).map(frame => { 315 | + const world = frame.worlds[IsolatedWorlds_js_1.MAIN_WORLD]; 316 | + const contextPayload = { 317 | + id: -1, 318 | + name: '', 319 | + auxData: { 320 | + frameId: frame._id, 321 | + } 322 | + }; 323 | + const context = new ExecutionContext_js_1.ExecutionContext(frame.client, 324 | + // @ts-ignore 325 | + contextPayload, world); 326 | + world.setContext(context); 327 | + }); 328 | + } 329 | } 330 | catch (error) { 331 | this.#frameTreeHandled?.resolve(); 332 | @@ -358,6 +389,23 @@ 333 | } 334 | this._frameTree.addFrame(frame); 335 | } 336 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events, 337 | + // instead, just mark it all empty 338 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 339 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`); 340 | + for (const worldSymbol of [IsolatedWorlds_js_1.MAIN_WORLD, IsolatedWorlds_js_1.PUPPETEER_WORLD]) { 341 | + // @ts-ignore 342 | + if (frame?.worlds[worldSymbol].context) { 343 | + // @ts-ignore 344 | + const frameOrWorker = frame.worlds[worldSymbol].environment; 345 | + if ('clearDocumentHandle' in frameOrWorker) { 346 | + frameOrWorker.clearDocumentHandle(); 347 | + } 348 | + // @ts-ignore 349 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === IsolatedWorlds_js_1.MAIN_WORLD ? -1 : -2); 350 | + } 351 | + } 352 | + } 353 | frame = await this._frameTree.waitForFrame(frameId); 354 | frame._navigated(framePayload); 355 | this.emit(FrameManagerEvents_js_1.FrameManagerEvent.FrameNavigated, frame); 356 | @@ -385,6 +433,24 @@ 357 | worldName: name, 358 | grantUniveralAccess: true, 359 | }) 360 | + .then((createIsolatedWorldResult) => { 361 | + // rebrowser-patches: save created context id 362 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 363 | + return; 364 | + } 365 | + if (!createIsolatedWorldResult?.executionContextId) { 366 | + // probably "Target closed" error, just ignore it 367 | + return; 368 | + } 369 | + // @ts-ignore 370 | + this.#onExecutionContextCreated({ 371 | + id: createIsolatedWorldResult.executionContextId, 372 | + name, 373 | + auxData: { 374 | + frameId: frame._id, 375 | + } 376 | + }, frame.client); 377 | + }) 378 | .catch(util_js_1.debugError); 379 | })); 380 | this.#isolatedWorlds.add(key); 381 | 382 | --- a/lib/cjs/puppeteer/cdp/IsolatedWorld.d.ts 383 | +++ b/lib/cjs/puppeteer/cdp/IsolatedWorld.d.ts 384 | @@ -12,9 +12,9 @@ 385 | import type { TimeoutSettings } from '../common/TimeoutSettings.js'; 386 | import type { EvaluateFunc, HandleFor } from '../common/types.js'; 387 | import { disposeSymbol } from '../util/disposable.js'; 388 | -import type { ExecutionContext } from './ExecutionContext.js'; 389 | +import { ExecutionContext } from './ExecutionContext.js'; 390 | import type { CdpFrame } from './Frame.js'; 391 | -import type { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js'; 392 | +import { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js'; 393 | import type { CdpWebWorker } from './WebWorker.js'; 394 | /** 395 | * @internal 396 | 397 | --- a/lib/cjs/puppeteer/cdp/IsolatedWorld.js 398 | +++ b/lib/cjs/puppeteer/cdp/IsolatedWorld.js 399 | @@ -1,4 +1,5 @@ 400 | "use strict"; 401 | +//@ts-nocheck 402 | /** 403 | * @license 404 | * Copyright 2019 Google Inc. 405 | @@ -12,6 +13,8 @@ 406 | const util_js_1 = require("../common/util.js"); 407 | const disposable_js_1 = require("../util/disposable.js"); 408 | const ElementHandle_js_1 = require("./ElementHandle.js"); 409 | +const ExecutionContext_js_1 = require("./ExecutionContext.js"); 410 | +const IsolatedWorlds_js_1 = require("./IsolatedWorlds.js"); 411 | const JSHandle_js_1 = require("./JSHandle.js"); 412 | /** 413 | * @internal 414 | @@ -70,6 +73,21 @@ 415 | * Waits for the next context to be set on the isolated world. 416 | */ 417 | async #waitForExecutionContext() { 418 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 419 | + if (fixMode === 'addBinding') { 420 | + const isMainWorld = this.#frameOrWorker.worlds[IsolatedWorlds_js_1.MAIN_WORLD] === this; 421 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${this.#frameOrWorker._id}, isMainWorld = ${isMainWorld}`); 422 | + const contextPayload = { 423 | + id: isMainWorld ? -1 : -2, 424 | + name: isMainWorld ? '' : util_js_1.UTILITY_WORLD_NAME, 425 | + auxData: { 426 | + frameId: this.#frameOrWorker._id, 427 | + } 428 | + }; 429 | + const context = new ExecutionContext_js_1.ExecutionContext(this.client, contextPayload, this); 430 | + this.setContext(context); 431 | + return context; 432 | + } 433 | const error = new Error('Execution context was destroyed'); 434 | const result = await (0, rxjs_js_1.firstValueFrom)((0, util_js_1.fromEmitterEvent)(this.#emitter, 'context').pipe((0, rxjs_js_1.raceWith)((0, util_js_1.fromEmitterEvent)(this.#emitter, 'disposed').pipe((0, rxjs_js_1.map)(() => { 435 | // The message has to match the CDP message expected by the WaitTask class. 436 | @@ -107,6 +125,8 @@ 437 | if (!context) { 438 | context = await this.#waitForExecutionContext(); 439 | } 440 | + // rebrowser-patches: make sure id is acquired 441 | + await context.acquireContextId(); 442 | const { object } = await this.client.send('DOM.resolveNode', { 443 | backendNodeId: backendNodeId, 444 | executionContextId: context.id, 445 | 446 | --- a/lib/cjs/puppeteer/cdp/WebWorker.js 447 | +++ b/lib/cjs/puppeteer/cdp/WebWorker.js 448 | @@ -24,6 +24,10 @@ 449 | this.#targetType = targetType; 450 | this.#world = new IsolatedWorld_js_1.IsolatedWorld(this, new TimeoutSettings_js_1.TimeoutSettings()); 451 | this.#client.once('Runtime.executionContextCreated', async (event) => { 452 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 453 | + // rebrowser-patches: ignore default logic 454 | + return; 455 | + } 456 | this.#world.setContext(new ExecutionContext_js_1.ExecutionContext(client, event.context, this.#world)); 457 | }); 458 | this.#world.emitter.on('consoleapicalled', async (event) => { 459 | @@ -42,7 +46,22 @@ 460 | }); 461 | // This might fail if the target is closed before we receive all execution contexts. 462 | networkManager?.addClient(this.#client).catch(util_js_1.debugError); 463 | - this.#client.send('Runtime.enable').catch(util_js_1.debugError); 464 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 465 | + // @ts-ignore 466 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo()); 467 | + // rebrowser-patches: manually create context 468 | + const contextPayload = { 469 | + id: -3, 470 | + auxData: { 471 | + frameId: targetId, 472 | + } 473 | + }; 474 | + // @ts-ignore 475 | + this.#world.setContext(new ExecutionContext_js_1.ExecutionContext(client, contextPayload, this.#world)); 476 | + } 477 | + else { 478 | + this.#client.send('Runtime.enable').catch(util_js_1.debugError); 479 | + } 480 | } 481 | mainRealm() { 482 | return this.#world; 483 | 484 | --- a/lib/cjs/puppeteer/common/util.js 485 | +++ b/lib/cjs/puppeteer/common/util.js 486 | @@ -252,7 +252,10 @@ 487 | /** 488 | * @internal 489 | */ 490 | -exports.UTILITY_WORLD_NAME = '__puppeteer_utility_world__' + version_js_1.packageVersion; 491 | +exports.UTILITY_WORLD_NAME = 492 | +// rebrowser-patches: change utility world name 493 | +process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') : 494 | + '__puppeteer_utility_world__' + version_js_1.packageVersion; 495 | /** 496 | * @internal 497 | */ 498 | @@ -261,6 +264,10 @@ 499 | * @internal 500 | */ 501 | function getSourceUrlComment(url) { 502 | + // rebrowser-patches: change sourceUrl to generic script name 503 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') { 504 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js'; 505 | + } 506 | return `//# sourceURL=${url}`; 507 | } 508 | /** 509 | 510 | --- a/lib/es5-iife/puppeteer-core-browser.js 511 | +++ b/lib/es5-iife/puppeteer-core-browser.js 512 | @@ -3561,7 +3561,9 @@ 513 | /** 514 | * @internal 515 | */ 516 | - const UTILITY_WORLD_NAME = '__puppeteer_utility_world__' + packageVersion; 517 | + const UTILITY_WORLD_NAME = 518 | + // rebrowser-patches: change utility world name 519 | + process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util' : '__puppeteer_utility_world__' + packageVersion; 520 | /** 521 | * @internal 522 | */ 523 | @@ -3570,6 +3572,10 @@ 524 | * @internal 525 | */ 526 | function getSourceUrlComment(url) { 527 | + // rebrowser-patches: change sourceUrl to generic script name 528 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') { 529 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js'; 530 | + } 531 | return `//# sourceURL=${url}`; 532 | } 533 | /** 534 | @@ -15625,6 +15631,7 @@ 535 | _classPrivateFieldInitSpec(this, _client8, void 0); 536 | _classPrivateFieldInitSpec(this, _world3, void 0); 537 | _classPrivateFieldInitSpec(this, _id5, void 0); 538 | + _defineProperty(this, "_frameId", void 0); 539 | _classPrivateFieldInitSpec(this, _name3, void 0); 540 | _classPrivateFieldInitSpec(this, _disposables, new DisposableStack()); 541 | // Contains mapping from functions that should be bound to Puppeteer functions. 542 | @@ -15640,16 +15647,22 @@ 543 | if (contextPayload.name) { 544 | _classPrivateFieldSet(_name3, this, contextPayload.name); 545 | } 546 | + // rebrowser-patches: keep frameId to use later 547 | + if (contextPayload.auxData?.frameId) { 548 | + this._frameId = contextPayload.auxData?.frameId; 549 | + } 550 | const clientEmitter = _classPrivateFieldGet(_disposables, this).use(new EventEmitter(_classPrivateFieldGet(_client8, this))); 551 | clientEmitter.on('Runtime.bindingCalled', _assertClassBrand(_ExecutionContext_brand, this, _onBindingCalled).bind(this)); 552 | - clientEmitter.on('Runtime.executionContextDestroyed', async event => { 553 | - if (event.executionContextId === _classPrivateFieldGet(_id5, this)) { 554 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 555 | + clientEmitter.on('Runtime.executionContextDestroyed', async event => { 556 | + if (event.executionContextId === _classPrivateFieldGet(_id5, this)) { 557 | + this[disposeSymbol](); 558 | + } 559 | + }); 560 | + clientEmitter.on('Runtime.executionContextsCleared', async () => { 561 | this[disposeSymbol](); 562 | - } 563 | - }); 564 | - clientEmitter.on('Runtime.executionContextsCleared', async () => { 565 | - this[disposeSymbol](); 566 | - }); 567 | + }); 568 | + } 569 | clientEmitter.on('Runtime.consoleAPICalled', _assertClassBrand(_ExecutionContext_brand, this, _onConsoleAPI).bind(this)); 570 | clientEmitter.on(exports.CDPSessionEvent.Disconnected, () => { 571 | this[disposeSymbol](); 572 | @@ -15771,6 +15784,181 @@ 573 | async evaluateHandle(pageFunction, ...args) { 574 | return await _assertClassBrand(_ExecutionContext_brand, this, _evaluate).call(this, false, pageFunction, ...args); 575 | } 576 | + // rebrowser-patches: alternative to dispose 577 | + clear(newId) { 578 | + _classPrivateFieldSet(_id5, this, newId); 579 | + _classPrivateFieldSet(_bindings, this, new Map()); 580 | + _classPrivateFieldSet(_bindingsInstalled, this, false); 581 | + _classPrivateFieldSet(_puppeteerUtil, this, undefined); 582 | + } 583 | + async __re__getMainWorld({ 584 | + client, 585 | + frameId, 586 | + isWorker = false 587 | + }) { 588 | + let contextId; 589 | + // random name to make it harder to detect for any 3rd party script by watching window object and events 590 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join(''); 591 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`); 592 | + // add the binding 593 | + await client.send('Runtime.addBinding', { 594 | + name: randomName 595 | + }); 596 | + // listen for 'Runtime.bindingCalled' event 597 | + const bindingCalledHandler = ({ 598 | + name, 599 | + payload, 600 | + executionContextId 601 | + }) => { 602 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', { 603 | + name, 604 | + payload, 605 | + executionContextId 606 | + }); 607 | + if (contextId > 0) { 608 | + // already acquired the id 609 | + return; 610 | + } 611 | + if (name !== randomName) { 612 | + // ignore irrelevant bindings 613 | + return; 614 | + } 615 | + if (payload !== frameId) { 616 | + // ignore irrelevant frames 617 | + return; 618 | + } 619 | + contextId = executionContextId; 620 | + // remove this listener 621 | + client.off('Runtime.bindingCalled', bindingCalledHandler); 622 | + }; 623 | + client.on('Runtime.bindingCalled', bindingCalledHandler); 624 | + if (isWorker) { 625 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate 626 | + await client.send('Runtime.evaluate', { 627 | + expression: `this['${randomName}']('${frameId}')` 628 | + }); 629 | + } else { 630 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent 631 | + await client.send('Page.addScriptToEvaluateOnNewDocument', { 632 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`, 633 | + runImmediately: true 634 | + }); 635 | + // create new isolated world for this frame 636 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 637 | + frameId, 638 | + // use randomName for worldName to distinguish from normal utility world 639 | + worldName: randomName, 640 | + grantUniveralAccess: true 641 | + }); 642 | + // emit event in the specific frame from the isolated world 643 | + await client.send('Runtime.evaluate', { 644 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`, 645 | + contextId: createIsolatedWorldResult.executionContextId 646 | + }); 647 | + } 648 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { 649 | + contextId 650 | + }); 651 | + return contextId; 652 | + } 653 | + async __re__getIsolatedWorld({ 654 | + client, 655 | + frameId, 656 | + worldName 657 | + }) { 658 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 659 | + frameId, 660 | + worldName, 661 | + grantUniveralAccess: true 662 | + }); 663 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult); 664 | + return createIsolatedWorldResult.executionContextId; 665 | + } 666 | + // rebrowser-patches: get context id if it's missing 667 | + async acquireContextId(tryCount = 1) { 668 | + if (_classPrivateFieldGet(_id5, this) > 0) { 669 | + return; 670 | + } 671 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 672 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${_classPrivateFieldGet(_id5, this)}, name = ${_classPrivateFieldGet(_name3, this)}, fixMode = ${fixMode}, tryCount = ${tryCount}`); 673 | + let contextId; 674 | + let tryAgain = true; 675 | + let errorMessage = 'N/A'; 676 | + if (fixMode === 'addBinding') { 677 | + try { 678 | + if (_classPrivateFieldGet(_id5, this) === -2) { 679 | + // isolated world 680 | + contextId = await this.__re__getIsolatedWorld({ 681 | + client: _classPrivateFieldGet(_client8, this), 682 | + frameId: this._frameId, 683 | + worldName: _classPrivateFieldGet(_name3, this) 684 | + }); 685 | + } else { 686 | + // main world 687 | + contextId = await this.__re__getMainWorld({ 688 | + client: _classPrivateFieldGet(_client8, this), 689 | + frameId: this._frameId, 690 | + isWorker: _classPrivateFieldGet(_id5, this) === -3 691 | + }); 692 | + } 693 | + } catch (error) { 694 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error); 695 | + errorMessage = error.message; 696 | + if (error instanceof Error) { 697 | + if (error.message.includes('No frame for given id found') || error.message.includes('Target closed') || error.message.includes('Session closed')) { 698 | + // target doesn't exist anymore, don't try again 699 | + tryAgain = false; 700 | + } 701 | + } 702 | + debugError(error); 703 | + } 704 | + } else if (fixMode === 'alwaysIsolated') { 705 | + if (_classPrivateFieldGet(_id5, this) === -3) { 706 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode'); 707 | + } 708 | + contextId = await this.__re__getIsolatedWorld({ 709 | + client: _classPrivateFieldGet(_client8, this), 710 | + frameId: this._frameId, 711 | + worldName: _classPrivateFieldGet(_name3, this) 712 | + }); 713 | + } else if (fixMode === 'enableDisable') { 714 | + const executionContextCreatedHandler = ({ 715 | + context 716 | + }) => { 717 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${_classPrivateFieldGet(_id5, this)}, name = ${_classPrivateFieldGet(_name3, this)}, contextId = ${contextId}, event.context.id = ${context.id}`); 718 | + if (contextId > 0) { 719 | + // already acquired the id 720 | + return; 721 | + } 722 | + if (_classPrivateFieldGet(_id5, this) === -1) { 723 | + // main world 724 | + if (context.auxData && context.auxData['isDefault']) { 725 | + contextId = context.id; 726 | + } 727 | + } else if (_classPrivateFieldGet(_id5, this) === -2) { 728 | + // utility world 729 | + if (_classPrivateFieldGet(_name3, this) === context.name) { 730 | + contextId = context.id; 731 | + } 732 | + } else if (_classPrivateFieldGet(_id5, this) === -3) { 733 | + // web worker 734 | + contextId = context.id; 735 | + } 736 | + }; 737 | + _classPrivateFieldGet(_client8, this).on('Runtime.executionContextCreated', executionContextCreatedHandler); 738 | + await _classPrivateFieldGet(_client8, this).send('Runtime.enable'); 739 | + await _classPrivateFieldGet(_client8, this).send('Runtime.disable'); 740 | + _classPrivateFieldGet(_client8, this).off('Runtime.executionContextCreated', executionContextCreatedHandler); 741 | + } 742 | + if (!contextId) { 743 | + if (!tryAgain || tryCount >= 3) { 744 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`); 745 | + } 746 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`); 747 | + return this.acquireContextId(tryCount + 1); 748 | + } 749 | + _classPrivateFieldSet(_id5, this, contextId); 750 | + } 751 | [disposeSymbol]() { 752 | _classPrivateFieldGet(_disposables, this).dispose(); 753 | this.emit('disposed', undefined); 754 | @@ -15870,6 +16058,12 @@ 755 | } 756 | } 757 | async function _evaluate(returnByValue, pageFunction, ...args) { 758 | + // rebrowser-patches: context id is missing, acquire it and try again 759 | + if (_classPrivateFieldGet(_id5, this) < 0) { 760 | + await this.acquireContextId(); 761 | + // @ts-ignore 762 | + return _assertClassBrand(_ExecutionContext_brand, this, _evaluate).call(this, returnByValue, pageFunction, ...args); 763 | + } 764 | const sourceUrlComment = getSourceUrlComment(getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ?? PuppeteerURL.INTERNAL_URL); 765 | if (isString(pageFunction)) { 766 | const contextId = _classPrivateFieldGet(_id5, this); 767 | @@ -16036,6 +16230,27 @@ 768 | 769 | /** 770 | * @license 771 | + * Copyright 2022 Google Inc. 772 | + * SPDX-License-Identifier: Apache-2.0 773 | + */ 774 | + /** 775 | + * A unique key for {@link IsolatedWorldChart} to denote the default world. 776 | + * Execution contexts are automatically created in the default world. 777 | + * 778 | + * @internal 779 | + */ 780 | + const MAIN_WORLD = Symbol('mainWorld'); 781 | + /** 782 | + * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world. 783 | + * This world contains all puppeteer-internal bindings/code. 784 | + * 785 | + * @internal 786 | + */ 787 | + const PUPPETEER_WORLD = Symbol('puppeteerWorld'); 788 | + 789 | + //@ts-nocheck 790 | + /** 791 | + * @license 792 | * Copyright 2019 Google Inc. 793 | * SPDX-License-Identifier: Apache-2.0 794 | */ 795 | @@ -16109,6 +16324,8 @@ 796 | if (!context) { 797 | context = await _assertClassBrand(_IsolatedWorld_brand, this, _waitForExecutionContext).call(this); 798 | } 799 | + // rebrowser-patches: make sure id is acquired 800 | + await context.acquireContextId(); 801 | const { 802 | object 803 | } = await this.client.send('DOM.resolveNode', { 804 | @@ -16164,15 +16381,9 @@ 805 | 806 | /** 807 | * @license 808 | - * Copyright 2022 Google Inc. 809 | + * Copyright 2019 Google Inc. 810 | * SPDX-License-Identifier: Apache-2.0 811 | */ 812 | - /** 813 | - * A unique key for {@link IsolatedWorldChart} to denote the default world. 814 | - * Execution contexts are automatically created in the default world. 815 | - * 816 | - * @internal 817 | - */ 818 | function _onContextDisposed() { 819 | _classPrivateFieldSet(_context, this, undefined); 820 | if ('clearDocumentHandle' in _classPrivateFieldGet(_frameOrWorker, this)) { 821 | @@ -16195,6 +16406,21 @@ 822 | * Waits for the next context to be set on the isolated world. 823 | */ 824 | async function _waitForExecutionContext() { 825 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 826 | + if (fixMode === 'addBinding') { 827 | + const isMainWorld = _classPrivateFieldGet(_frameOrWorker, this).worlds[MAIN_WORLD] === this; 828 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${_classPrivateFieldGet(_frameOrWorker, this)._id}, isMainWorld = ${isMainWorld}`); 829 | + const contextPayload = { 830 | + id: isMainWorld ? -1 : -2, 831 | + name: isMainWorld ? '' : UTILITY_WORLD_NAME, 832 | + auxData: { 833 | + frameId: _classPrivateFieldGet(_frameOrWorker, this)._id 834 | + } 835 | + }; 836 | + const context = new ExecutionContext(this.client, contextPayload, this); 837 | + this.setContext(context); 838 | + return context; 839 | + } 840 | const error = new Error('Execution context was destroyed'); 841 | const result = await firstValueFrom(fromEmitterEvent(_classPrivateFieldGet(_emitter2, this), 'context').pipe(raceWith(fromEmitterEvent(_classPrivateFieldGet(_emitter2, this), 'disposed').pipe(map(() => { 842 | // The message has to match the CDP message expected by the WaitTask class. 843 | @@ -16202,20 +16428,6 @@ 844 | })), timeout(this.timeoutSettings.timeout())))); 845 | return result; 846 | } 847 | - const MAIN_WORLD = Symbol('mainWorld'); 848 | - /** 849 | - * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world. 850 | - * This world contains all puppeteer-internal bindings/code. 851 | - * 852 | - * @internal 853 | - */ 854 | - const PUPPETEER_WORLD = Symbol('puppeteerWorld'); 855 | - 856 | - /** 857 | - * @license 858 | - * Copyright 2019 Google Inc. 859 | - * SPDX-License-Identifier: Apache-2.0 860 | - */ 861 | const puppeteerToProtocolLifecycle = new Map([['load', 'load'], ['domcontentloaded', 'DOMContentLoaded'], ['networkidle0', 'networkIdle'], ['networkidle2', 'networkAlmostIdle']]); 862 | /** 863 | * @internal 864 | @@ -18012,6 +18224,10 @@ 865 | _assertClassBrand(_FrameManager_brand, this, _onFrameStoppedLoading).call(this, event.frameId); 866 | }); 867 | session.on('Runtime.executionContextCreated', async event => { 868 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 869 | + // rebrowser-patches: ignore default logic 870 | + return; 871 | + } 872 | await _classPrivateFieldGet(_frameTreeHandled, this)?.valueOrThrow(); 873 | _assertClassBrand(_FrameManager_brand, this, _onExecutionContextCreated).call(this, event.context, session); 874 | }); 875 | @@ -18035,13 +18251,39 @@ 876 | _classPrivateFieldGet(_frameTreeHandled, this)?.resolve(); 877 | }), client.send('Page.setLifecycleEventsEnabled', { 878 | enabled: true 879 | - }), client.send('Runtime.enable').then(() => { 880 | - return _assertClassBrand(_FrameManager_brand, this, _createIsolatedWorld).call(this, client, UTILITY_WORLD_NAME); 881 | - }), ...(frame ? Array.from(_classPrivateFieldGet(_scriptsToEvaluateOnNewDocument, this).values()) : []).map(script => { 882 | + }), (() => { 883 | + // rebrowser-patches: skip Runtime.enable 884 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 885 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize'); 886 | + return _assertClassBrand(_FrameManager_brand, this, _createIsolatedWorld).call(this, client, UTILITY_WORLD_NAME); 887 | + } 888 | + return client.send('Runtime.enable').then(() => { 889 | + return _assertClassBrand(_FrameManager_brand, this, _createIsolatedWorld).call(this, client, UTILITY_WORLD_NAME); 890 | + }); 891 | + })(), ...(frame ? Array.from(_classPrivateFieldGet(_scriptsToEvaluateOnNewDocument, this).values()) : []).map(script => { 892 | return frame?.addPreloadScript(script); 893 | }), ...(frame ? Array.from(_classPrivateFieldGet(_bindings2, this).values()) : []).map(binding => { 894 | return frame?.addExposedFunctionBinding(binding); 895 | })]); 896 | + // rebrowser-patches: manually create main world context 897 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 898 | + this.frames().filter(frame => { 899 | + return frame.client === client; 900 | + }).map(frame => { 901 | + const world = frame.worlds[MAIN_WORLD]; 902 | + const contextPayload = { 903 | + id: -1, 904 | + name: '', 905 | + auxData: { 906 | + frameId: frame._id 907 | + } 908 | + }; 909 | + const context = new ExecutionContext(frame.client, 910 | + // @ts-ignore 911 | + contextPayload, world); 912 | + world.setContext(context); 913 | + }); 914 | + } 915 | } catch (error) { 916 | _classPrivateFieldGet(_frameTreeHandled, this)?.resolve(); 917 | // The target might have been closed before the initialization finished. 918 | @@ -18243,6 +18485,23 @@ 919 | } 920 | this._frameTree.addFrame(frame); 921 | } 922 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events, 923 | + // instead, just mark it all empty 924 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 925 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`); 926 | + for (const worldSymbol of [MAIN_WORLD, PUPPETEER_WORLD]) { 927 | + // @ts-ignore 928 | + if (frame?.worlds[worldSymbol].context) { 929 | + // @ts-ignore 930 | + const frameOrWorker = frame.worlds[worldSymbol].environment; 931 | + if ('clearDocumentHandle' in frameOrWorker) { 932 | + frameOrWorker.clearDocumentHandle(); 933 | + } 934 | + // @ts-ignore 935 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === MAIN_WORLD ? -1 : -2); 936 | + } 937 | + } 938 | + } 939 | frame = await this._frameTree.waitForFrame(frameId); 940 | frame._navigated(framePayload); 941 | this.emit(exports.FrameManagerEvent.FrameNavigated, frame); 942 | @@ -18266,6 +18525,23 @@ 943 | frameId: frame._id, 944 | worldName: name, 945 | grantUniveralAccess: true 946 | + }).then(createIsolatedWorldResult => { 947 | + // rebrowser-patches: save created context id 948 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 949 | + return; 950 | + } 951 | + if (!createIsolatedWorldResult?.executionContextId) { 952 | + // probably "Target closed" error, just ignore it 953 | + return; 954 | + } 955 | + // @ts-ignore 956 | + _assertClassBrand(_FrameManager_brand, this, _onExecutionContextCreated).call(this, { 957 | + id: createIsolatedWorldResult.executionContextId, 958 | + name, 959 | + auxData: { 960 | + frameId: frame._id 961 | + } 962 | + }, frame.client); 963 | }).catch(debugError); 964 | })); 965 | _classPrivateFieldGet(_isolatedWorlds, this).add(key); 966 | @@ -20394,6 +20670,10 @@ 967 | _classPrivateFieldSet(_targetType2, this, targetType); 968 | _classPrivateFieldSet(_world4, this, new IsolatedWorld(this, new TimeoutSettings())); 969 | _classPrivateFieldGet(_client17, this).once('Runtime.executionContextCreated', async event => { 970 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 971 | + // rebrowser-patches: ignore default logic 972 | + return; 973 | + } 974 | _classPrivateFieldGet(_world4, this).setContext(new ExecutionContext(client, event.context, _classPrivateFieldGet(_world4, this))); 975 | }); 976 | _classPrivateFieldGet(_world4, this).emitter.on('consoleapicalled', async event => { 977 | @@ -20411,7 +20691,21 @@ 978 | }); 979 | // This might fail if the target is closed before we receive all execution contexts. 980 | networkManager?.addClient(_classPrivateFieldGet(_client17, this)).catch(debugError); 981 | - _classPrivateFieldGet(_client17, this).send('Runtime.enable').catch(debugError); 982 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 983 | + // @ts-ignore 984 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo()); 985 | + // rebrowser-patches: manually create context 986 | + const contextPayload = { 987 | + id: -3, 988 | + auxData: { 989 | + frameId: targetId 990 | + } 991 | + }; 992 | + // @ts-ignore 993 | + _classPrivateFieldGet(_world4, this).setContext(new ExecutionContext(client, contextPayload, _classPrivateFieldGet(_world4, this))); 994 | + } else { 995 | + _classPrivateFieldGet(_client17, this).send('Runtime.enable').catch(debugError); 996 | + } 997 | } 998 | mainRealm() { 999 | return _classPrivateFieldGet(_world4, this); 1000 | @@ -22313,6 +22607,10 @@ 1001 | async newPage() { 1002 | return await _classPrivateFieldGet(_defaultContext, this).newPage(); 1003 | } 1004 | + // rebrowser-patches: expose browser CDP session 1005 | + _connection() { 1006 | + return _classPrivateFieldGet(_connection4, this); 1007 | + } 1008 | async _createPageInContext(contextId) { 1009 | const { 1010 | targetId 1011 | 1012 | --- a/lib/esm/puppeteer/cdp/Browser.d.ts 1013 | +++ b/lib/esm/puppeteer/cdp/Browser.d.ts 1014 | @@ -32,6 +32,7 @@ 1015 | _disposeContext(contextId?: string): Promise; 1016 | wsEndpoint(): string; 1017 | newPage(): Promise; 1018 | + _connection(): Connection; 1019 | _createPageInContext(contextId?: string): Promise; 1020 | installExtension(path: string): Promise; 1021 | uninstallExtension(id: string): Promise; 1022 | 1023 | --- a/lib/esm/puppeteer/cdp/Browser.js 1024 | +++ b/lib/esm/puppeteer/cdp/Browser.js 1025 | @@ -172,6 +172,10 @@ 1026 | async newPage() { 1027 | return await this.#defaultContext.newPage(); 1028 | } 1029 | + // rebrowser-patches: expose browser CDP session 1030 | + _connection() { 1031 | + return this.#connection; 1032 | + } 1033 | async _createPageInContext(contextId) { 1034 | const { targetId } = await this.#connection.send('Target.createTarget', { 1035 | url: 'about:blank', 1036 | 1037 | --- a/lib/esm/puppeteer/cdp/ExecutionContext.d.ts 1038 | +++ b/lib/esm/puppeteer/cdp/ExecutionContext.d.ts 1039 | @@ -22,6 +22,7 @@ 1040 | bindingcalled: Protocol.Runtime.BindingCalledEvent; 1041 | }> implements Disposable { 1042 | #private; 1043 | + _frameId: any; 1044 | constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, world: IsolatedWorld); 1045 | get id(): number; 1046 | get puppeteerUtil(): Promise>; 1047 | @@ -116,6 +117,10 @@ 1048 | * {@link ElementHandle | element handle}. 1049 | */ 1050 | evaluateHandle = EvaluateFunc>(pageFunction: Func | string, ...args: Params): Promise>>>; 1051 | + clear(newId: any): void; 1052 | + __re__getMainWorld({ client, frameId, isWorker }: any): Promise; 1053 | + __re__getIsolatedWorld({ client, frameId, worldName }: any): Promise; 1054 | + acquireContextId(tryCount?: number): Promise; 1055 | [disposeSymbol](): void; 1056 | } 1057 | //# sourceMappingURL=ExecutionContext.d.ts.map 1058 | \ No newline at end of file 1059 | 1060 | --- a/lib/esm/puppeteer/cdp/ExecutionContext.js 1061 | +++ b/lib/esm/puppeteer/cdp/ExecutionContext.js 1062 | @@ -83,6 +83,7 @@ 1063 | #client; 1064 | #world; 1065 | #id; 1066 | + _frameId; 1067 | #name; 1068 | #disposables = new DisposableStack(); 1069 | constructor(client, contextPayload, world) { 1070 | @@ -93,16 +94,22 @@ 1071 | if (contextPayload.name) { 1072 | this.#name = contextPayload.name; 1073 | } 1074 | + // rebrowser-patches: keep frameId to use later 1075 | + if (contextPayload.auxData?.frameId) { 1076 | + this._frameId = contextPayload.auxData?.frameId; 1077 | + } 1078 | const clientEmitter = this.#disposables.use(new EventEmitter(this.#client)); 1079 | clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this)); 1080 | - clientEmitter.on('Runtime.executionContextDestroyed', async (event) => { 1081 | - if (event.executionContextId === this.#id) { 1082 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 1083 | + clientEmitter.on('Runtime.executionContextDestroyed', async (event) => { 1084 | + if (event.executionContextId === this.#id) { 1085 | + this[disposeSymbol](); 1086 | + } 1087 | + }); 1088 | + clientEmitter.on('Runtime.executionContextsCleared', async () => { 1089 | this[disposeSymbol](); 1090 | - } 1091 | - }); 1092 | - clientEmitter.on('Runtime.executionContextsCleared', async () => { 1093 | - this[disposeSymbol](); 1094 | - }); 1095 | + }); 1096 | + } 1097 | clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this)); 1098 | clientEmitter.on(CDPSessionEvent.Disconnected, () => { 1099 | this[disposeSymbol](); 1100 | @@ -325,7 +332,181 @@ 1101 | async evaluateHandle(pageFunction, ...args) { 1102 | return await this.#evaluate(false, pageFunction, ...args); 1103 | } 1104 | + // rebrowser-patches: alternative to dispose 1105 | + clear(newId) { 1106 | + this.#id = newId; 1107 | + this.#bindings = new Map(); 1108 | + this.#bindingsInstalled = false; 1109 | + this.#puppeteerUtil = undefined; 1110 | + } 1111 | + async __re__getMainWorld({ client, frameId, isWorker = false }) { 1112 | + let contextId; 1113 | + // random name to make it harder to detect for any 3rd party script by watching window object and events 1114 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join(''); 1115 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`); 1116 | + // add the binding 1117 | + await client.send('Runtime.addBinding', { 1118 | + name: randomName, 1119 | + }); 1120 | + // listen for 'Runtime.bindingCalled' event 1121 | + const bindingCalledHandler = ({ name, payload, executionContextId }) => { 1122 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', { 1123 | + name, 1124 | + payload, 1125 | + executionContextId 1126 | + }); 1127 | + if (contextId > 0) { 1128 | + // already acquired the id 1129 | + return; 1130 | + } 1131 | + if (name !== randomName) { 1132 | + // ignore irrelevant bindings 1133 | + return; 1134 | + } 1135 | + if (payload !== frameId) { 1136 | + // ignore irrelevant frames 1137 | + return; 1138 | + } 1139 | + contextId = executionContextId; 1140 | + // remove this listener 1141 | + client.off('Runtime.bindingCalled', bindingCalledHandler); 1142 | + }; 1143 | + client.on('Runtime.bindingCalled', bindingCalledHandler); 1144 | + if (isWorker) { 1145 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate 1146 | + await client.send('Runtime.evaluate', { 1147 | + expression: `this['${randomName}']('${frameId}')`, 1148 | + }); 1149 | + } 1150 | + else { 1151 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent 1152 | + await client.send('Page.addScriptToEvaluateOnNewDocument', { 1153 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`, 1154 | + runImmediately: true, 1155 | + }); 1156 | + // create new isolated world for this frame 1157 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 1158 | + frameId, 1159 | + // use randomName for worldName to distinguish from normal utility world 1160 | + worldName: randomName, 1161 | + grantUniveralAccess: true, 1162 | + }); 1163 | + // emit event in the specific frame from the isolated world 1164 | + await client.send('Runtime.evaluate', { 1165 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`, 1166 | + contextId: createIsolatedWorldResult.executionContextId, 1167 | + }); 1168 | + } 1169 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId }); 1170 | + return contextId; 1171 | + } 1172 | + async __re__getIsolatedWorld({ client, frameId, worldName }) { 1173 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 1174 | + frameId, 1175 | + worldName, 1176 | + grantUniveralAccess: true, 1177 | + }); 1178 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult); 1179 | + return createIsolatedWorldResult.executionContextId; 1180 | + } 1181 | + // rebrowser-patches: get context id if it's missing 1182 | + async acquireContextId(tryCount = 1) { 1183 | + if (this.#id > 0) { 1184 | + return; 1185 | + } 1186 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 1187 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${this.#id}, name = ${this.#name}, fixMode = ${fixMode}, tryCount = ${tryCount}`); 1188 | + let contextId; 1189 | + let tryAgain = true; 1190 | + let errorMessage = 'N/A'; 1191 | + if (fixMode === 'addBinding') { 1192 | + try { 1193 | + if (this.#id === -2) { 1194 | + // isolated world 1195 | + contextId = await this.__re__getIsolatedWorld({ 1196 | + client: this.#client, 1197 | + frameId: this._frameId, 1198 | + worldName: this.#name, 1199 | + }); 1200 | + } 1201 | + else { 1202 | + // main world 1203 | + contextId = await this.__re__getMainWorld({ 1204 | + client: this.#client, 1205 | + frameId: this._frameId, 1206 | + isWorker: this.#id === -3, 1207 | + }); 1208 | + } 1209 | + } 1210 | + catch (error) { 1211 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error); 1212 | + errorMessage = error.message; 1213 | + if (error instanceof Error) { 1214 | + if (error.message.includes('No frame for given id found') || 1215 | + error.message.includes('Target closed') || 1216 | + error.message.includes('Session closed')) { 1217 | + // target doesn't exist anymore, don't try again 1218 | + tryAgain = false; 1219 | + } 1220 | + } 1221 | + debugError(error); 1222 | + } 1223 | + } 1224 | + else if (fixMode === 'alwaysIsolated') { 1225 | + if (this.#id === -3) { 1226 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode'); 1227 | + } 1228 | + contextId = await this.__re__getIsolatedWorld({ 1229 | + client: this.#client, 1230 | + frameId: this._frameId, 1231 | + worldName: this.#name, 1232 | + }); 1233 | + } 1234 | + else if (fixMode === 'enableDisable') { 1235 | + const executionContextCreatedHandler = ({ context }) => { 1236 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${this.#id}, name = ${this.#name}, contextId = ${contextId}, event.context.id = ${context.id}`); 1237 | + if (contextId > 0) { 1238 | + // already acquired the id 1239 | + return; 1240 | + } 1241 | + if (this.#id === -1) { 1242 | + // main world 1243 | + if (context.auxData && context.auxData['isDefault']) { 1244 | + contextId = context.id; 1245 | + } 1246 | + } 1247 | + else if (this.#id === -2) { 1248 | + // utility world 1249 | + if (this.#name === context.name) { 1250 | + contextId = context.id; 1251 | + } 1252 | + } 1253 | + else if (this.#id === -3) { 1254 | + // web worker 1255 | + contextId = context.id; 1256 | + } 1257 | + }; 1258 | + this.#client.on('Runtime.executionContextCreated', executionContextCreatedHandler); 1259 | + await this.#client.send('Runtime.enable'); 1260 | + await this.#client.send('Runtime.disable'); 1261 | + this.#client.off('Runtime.executionContextCreated', executionContextCreatedHandler); 1262 | + } 1263 | + if (!contextId) { 1264 | + if (!tryAgain || tryCount >= 3) { 1265 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`); 1266 | + } 1267 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`); 1268 | + return this.acquireContextId(tryCount + 1); 1269 | + } 1270 | + this.#id = contextId; 1271 | + } 1272 | async #evaluate(returnByValue, pageFunction, ...args) { 1273 | + // rebrowser-patches: context id is missing, acquire it and try again 1274 | + if (this.#id < 0) { 1275 | + await this.acquireContextId(); 1276 | + // @ts-ignore 1277 | + return this.#evaluate(returnByValue, pageFunction, ...args); 1278 | + } 1279 | const sourceUrlComment = getSourceUrlComment(getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ?? 1280 | PuppeteerURL.INTERNAL_URL); 1281 | if (isString(pageFunction)) { 1282 | 1283 | --- a/lib/esm/puppeteer/cdp/FrameManager.js 1284 | +++ b/lib/esm/puppeteer/cdp/FrameManager.js 1285 | @@ -151,6 +151,10 @@ 1286 | this.#onFrameStoppedLoading(event.frameId); 1287 | }); 1288 | session.on('Runtime.executionContextCreated', async (event) => { 1289 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 1290 | + // rebrowser-patches: ignore default logic 1291 | + return; 1292 | + } 1293 | await this.#frameTreeHandled?.valueOrThrow(); 1294 | this.#onExecutionContextCreated(event.context, session); 1295 | }); 1296 | @@ -175,9 +179,16 @@ 1297 | this.#frameTreeHandled?.resolve(); 1298 | }), 1299 | client.send('Page.setLifecycleEventsEnabled', { enabled: true }), 1300 | - client.send('Runtime.enable').then(() => { 1301 | - return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); 1302 | - }), 1303 | + (() => { 1304 | + // rebrowser-patches: skip Runtime.enable 1305 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 1306 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize'); 1307 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); 1308 | + } 1309 | + return client.send('Runtime.enable').then(() => { 1310 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); 1311 | + }); 1312 | + })(), 1313 | ...(frame 1314 | ? Array.from(this.#scriptsToEvaluateOnNewDocument.values()) 1315 | : []).map(script => { 1316 | @@ -187,6 +198,26 @@ 1317 | return frame?.addExposedFunctionBinding(binding); 1318 | }), 1319 | ]); 1320 | + // rebrowser-patches: manually create main world context 1321 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 1322 | + this.frames() 1323 | + .filter(frame => { 1324 | + return frame.client === client; 1325 | + }).map(frame => { 1326 | + const world = frame.worlds[MAIN_WORLD]; 1327 | + const contextPayload = { 1328 | + id: -1, 1329 | + name: '', 1330 | + auxData: { 1331 | + frameId: frame._id, 1332 | + } 1333 | + }; 1334 | + const context = new ExecutionContext(frame.client, 1335 | + // @ts-ignore 1336 | + contextPayload, world); 1337 | + world.setContext(context); 1338 | + }); 1339 | + } 1340 | } 1341 | catch (error) { 1342 | this.#frameTreeHandled?.resolve(); 1343 | @@ -355,6 +386,23 @@ 1344 | } 1345 | this._frameTree.addFrame(frame); 1346 | } 1347 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events, 1348 | + // instead, just mark it all empty 1349 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 1350 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`); 1351 | + for (const worldSymbol of [MAIN_WORLD, PUPPETEER_WORLD]) { 1352 | + // @ts-ignore 1353 | + if (frame?.worlds[worldSymbol].context) { 1354 | + // @ts-ignore 1355 | + const frameOrWorker = frame.worlds[worldSymbol].environment; 1356 | + if ('clearDocumentHandle' in frameOrWorker) { 1357 | + frameOrWorker.clearDocumentHandle(); 1358 | + } 1359 | + // @ts-ignore 1360 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === MAIN_WORLD ? -1 : -2); 1361 | + } 1362 | + } 1363 | + } 1364 | frame = await this._frameTree.waitForFrame(frameId); 1365 | frame._navigated(framePayload); 1366 | this.emit(FrameManagerEvent.FrameNavigated, frame); 1367 | @@ -382,6 +430,24 @@ 1368 | worldName: name, 1369 | grantUniveralAccess: true, 1370 | }) 1371 | + .then((createIsolatedWorldResult) => { 1372 | + // rebrowser-patches: save created context id 1373 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 1374 | + return; 1375 | + } 1376 | + if (!createIsolatedWorldResult?.executionContextId) { 1377 | + // probably "Target closed" error, just ignore it 1378 | + return; 1379 | + } 1380 | + // @ts-ignore 1381 | + this.#onExecutionContextCreated({ 1382 | + id: createIsolatedWorldResult.executionContextId, 1383 | + name, 1384 | + auxData: { 1385 | + frameId: frame._id, 1386 | + } 1387 | + }, frame.client); 1388 | + }) 1389 | .catch(debugError); 1390 | })); 1391 | this.#isolatedWorlds.add(key); 1392 | 1393 | --- a/lib/esm/puppeteer/cdp/IsolatedWorld.d.ts 1394 | +++ b/lib/esm/puppeteer/cdp/IsolatedWorld.d.ts 1395 | @@ -12,9 +12,9 @@ 1396 | import type { TimeoutSettings } from '../common/TimeoutSettings.js'; 1397 | import type { EvaluateFunc, HandleFor } from '../common/types.js'; 1398 | import { disposeSymbol } from '../util/disposable.js'; 1399 | -import type { ExecutionContext } from './ExecutionContext.js'; 1400 | +import { ExecutionContext } from './ExecutionContext.js'; 1401 | import type { CdpFrame } from './Frame.js'; 1402 | -import type { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js'; 1403 | +import { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js'; 1404 | import type { CdpWebWorker } from './WebWorker.js'; 1405 | /** 1406 | * @internal 1407 | 1408 | --- a/lib/esm/puppeteer/cdp/IsolatedWorld.js 1409 | +++ b/lib/esm/puppeteer/cdp/IsolatedWorld.js 1410 | @@ -1,3 +1,4 @@ 1411 | +//@ts-nocheck 1412 | /** 1413 | * @license 1414 | * Copyright 2019 Google Inc. 1415 | @@ -6,9 +7,11 @@ 1416 | import { firstValueFrom, map, raceWith } from '../../third_party/rxjs/rxjs.js'; 1417 | import { Realm } from '../api/Realm.js'; 1418 | import { EventEmitter } from '../common/EventEmitter.js'; 1419 | -import { fromEmitterEvent, timeout, withSourcePuppeteerURLIfNone, } from '../common/util.js'; 1420 | +import { fromEmitterEvent, timeout, withSourcePuppeteerURLIfNone, UTILITY_WORLD_NAME, } from '../common/util.js'; 1421 | import { disposeSymbol } from '../util/disposable.js'; 1422 | import { CdpElementHandle } from './ElementHandle.js'; 1423 | +import { ExecutionContext } from './ExecutionContext.js'; 1424 | +import { MAIN_WORLD, PUPPETEER_WORLD } from './IsolatedWorlds.js'; 1425 | import { CdpJSHandle } from './JSHandle.js'; 1426 | /** 1427 | * @internal 1428 | @@ -67,6 +70,21 @@ 1429 | * Waits for the next context to be set on the isolated world. 1430 | */ 1431 | async #waitForExecutionContext() { 1432 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 1433 | + if (fixMode === 'addBinding') { 1434 | + const isMainWorld = this.#frameOrWorker.worlds[MAIN_WORLD] === this; 1435 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${this.#frameOrWorker._id}, isMainWorld = ${isMainWorld}`); 1436 | + const contextPayload = { 1437 | + id: isMainWorld ? -1 : -2, 1438 | + name: isMainWorld ? '' : UTILITY_WORLD_NAME, 1439 | + auxData: { 1440 | + frameId: this.#frameOrWorker._id, 1441 | + } 1442 | + }; 1443 | + const context = new ExecutionContext(this.client, contextPayload, this); 1444 | + this.setContext(context); 1445 | + return context; 1446 | + } 1447 | const error = new Error('Execution context was destroyed'); 1448 | const result = await firstValueFrom(fromEmitterEvent(this.#emitter, 'context').pipe(raceWith(fromEmitterEvent(this.#emitter, 'disposed').pipe(map(() => { 1449 | // The message has to match the CDP message expected by the WaitTask class. 1450 | @@ -104,6 +122,8 @@ 1451 | if (!context) { 1452 | context = await this.#waitForExecutionContext(); 1453 | } 1454 | + // rebrowser-patches: make sure id is acquired 1455 | + await context.acquireContextId(); 1456 | const { object } = await this.client.send('DOM.resolveNode', { 1457 | backendNodeId: backendNodeId, 1458 | executionContextId: context.id, 1459 | 1460 | --- a/lib/esm/puppeteer/cdp/WebWorker.js 1461 | +++ b/lib/esm/puppeteer/cdp/WebWorker.js 1462 | @@ -21,6 +21,10 @@ 1463 | this.#targetType = targetType; 1464 | this.#world = new IsolatedWorld(this, new TimeoutSettings()); 1465 | this.#client.once('Runtime.executionContextCreated', async (event) => { 1466 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 1467 | + // rebrowser-patches: ignore default logic 1468 | + return; 1469 | + } 1470 | this.#world.setContext(new ExecutionContext(client, event.context, this.#world)); 1471 | }); 1472 | this.#world.emitter.on('consoleapicalled', async (event) => { 1473 | @@ -39,7 +43,22 @@ 1474 | }); 1475 | // This might fail if the target is closed before we receive all execution contexts. 1476 | networkManager?.addClient(this.#client).catch(debugError); 1477 | - this.#client.send('Runtime.enable').catch(debugError); 1478 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 1479 | + // @ts-ignore 1480 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo()); 1481 | + // rebrowser-patches: manually create context 1482 | + const contextPayload = { 1483 | + id: -3, 1484 | + auxData: { 1485 | + frameId: targetId, 1486 | + } 1487 | + }; 1488 | + // @ts-ignore 1489 | + this.#world.setContext(new ExecutionContext(client, contextPayload, this.#world)); 1490 | + } 1491 | + else { 1492 | + this.#client.send('Runtime.enable').catch(debugError); 1493 | + } 1494 | } 1495 | mainRealm() { 1496 | return this.#world; 1497 | 1498 | --- a/lib/esm/puppeteer/common/util.js 1499 | +++ b/lib/esm/puppeteer/common/util.js 1500 | @@ -231,7 +231,10 @@ 1501 | /** 1502 | * @internal 1503 | */ 1504 | -export const UTILITY_WORLD_NAME = '__puppeteer_utility_world__' + packageVersion; 1505 | +export const UTILITY_WORLD_NAME = 1506 | +// rebrowser-patches: change utility world name 1507 | +process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') : 1508 | + '__puppeteer_utility_world__' + packageVersion; 1509 | /** 1510 | * @internal 1511 | */ 1512 | @@ -240,6 +243,10 @@ 1513 | * @internal 1514 | */ 1515 | export function getSourceUrlComment(url) { 1516 | + // rebrowser-patches: change sourceUrl to generic script name 1517 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') { 1518 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js'; 1519 | + } 1520 | return `//# sourceURL=${url}`; 1521 | } 1522 | /** 1523 | -------------------------------------------------------------------------------- /patches/puppeteer-core/src.patch: -------------------------------------------------------------------------------- 1 | --- a/src/cdp/Browser.ts 2 | +++ b/src/cdp/Browser.ts 3 | @@ -334,6 +334,11 @@ 4 | return await this.#defaultContext.newPage(); 5 | } 6 | 7 | + // rebrowser-patches: expose browser CDP session 8 | + _connection(): Connection { 9 | + return this.#connection; 10 | + } 11 | + 12 | async _createPageInContext(contextId?: string): Promise { 13 | const {targetId} = await this.#connection.send('Target.createTarget', { 14 | url: 'about:blank', 15 | 16 | --- a/src/cdp/ExecutionContext.ts 17 | +++ b/src/cdp/ExecutionContext.ts 18 | @@ -78,6 +78,7 @@ 19 | #client: CDPSession; 20 | #world: IsolatedWorld; 21 | #id: number; 22 | + _frameId: any; 23 | #name?: string; 24 | 25 | readonly #disposables = new DisposableStack(); 26 | @@ -94,16 +95,22 @@ 27 | if (contextPayload.name) { 28 | this.#name = contextPayload.name; 29 | } 30 | + // rebrowser-patches: keep frameId to use later 31 | + if (contextPayload.auxData?.frameId) { 32 | + this._frameId = contextPayload.auxData?.frameId; 33 | + } 34 | const clientEmitter = this.#disposables.use(new EventEmitter(this.#client)); 35 | clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this)); 36 | - clientEmitter.on('Runtime.executionContextDestroyed', async event => { 37 | - if (event.executionContextId === this.#id) { 38 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 39 | + clientEmitter.on('Runtime.executionContextDestroyed', async event => { 40 | + if (event.executionContextId === this.#id) { 41 | + this[disposeSymbol](); 42 | + } 43 | + }); 44 | + clientEmitter.on('Runtime.executionContextsCleared', async () => { 45 | this[disposeSymbol](); 46 | - } 47 | - }); 48 | - clientEmitter.on('Runtime.executionContextsCleared', async () => { 49 | - this[disposeSymbol](); 50 | - }); 51 | + }); 52 | + } 53 | clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this)); 54 | clientEmitter.on(CDPSessionEvent.Disconnected, () => { 55 | this[disposeSymbol](); 56 | @@ -350,6 +357,186 @@ 57 | return await this.#evaluate(false, pageFunction, ...args); 58 | } 59 | 60 | + // rebrowser-patches: alternative to dispose 61 | + clear(newId: any) { 62 | + this.#id = newId; 63 | + this.#bindings = new Map(); 64 | + this.#bindingsInstalled = false; 65 | + this.#puppeteerUtil = undefined; 66 | + } 67 | + async __re__getMainWorld({ client, frameId, isWorker = false }: any) { 68 | + let contextId: any; 69 | + 70 | + // random name to make it harder to detect for any 3rd party script by watching window object and events 71 | + const randomName = [...Array(Math.floor(Math.random() * (10 + 1)) + 10)].map(() => Math.random().toString(36)[2]).join(''); 72 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] binding name = ${randomName}`); 73 | + 74 | + // add the binding 75 | + await client.send('Runtime.addBinding', { 76 | + name: randomName, 77 | + }); 78 | + 79 | + // listen for 'Runtime.bindingCalled' event 80 | + const bindingCalledHandler = ({ name, payload, executionContextId }: any) => { 81 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][bindingCalledHandler]', { 82 | + name, 83 | + payload, 84 | + executionContextId 85 | + }); 86 | + if (contextId > 0) { 87 | + // already acquired the id 88 | + return; 89 | + } 90 | + if (name !== randomName) { 91 | + // ignore irrelevant bindings 92 | + return; 93 | + } 94 | + if (payload !== frameId) { 95 | + // ignore irrelevant frames 96 | + return; 97 | + } 98 | + contextId = executionContextId; 99 | + // remove this listener 100 | + client.off('Runtime.bindingCalled', bindingCalledHandler); 101 | + }; 102 | + client.on('Runtime.bindingCalled', bindingCalledHandler); 103 | + 104 | + if (isWorker) { 105 | + // workers don't support `Page.addScriptToEvaluateOnNewDocument` and `Page.createIsolatedWorld`, but there are no iframes inside of them, so it's safe to just use Runtime.evaluate 106 | + await client.send('Runtime.evaluate', { 107 | + expression: `this['${randomName}']('${frameId}')`, 108 | + }); 109 | + } else { 110 | + // we could call the binding right from `addScriptToEvaluateOnNewDocument`, but this way it will be called in all existing frames and it's hard to distinguish children from the parent 111 | + await client.send('Page.addScriptToEvaluateOnNewDocument', { 112 | + source: `document.addEventListener('${randomName}', (e) => self['${randomName}'](e.detail.frameId))`, 113 | + runImmediately: true, 114 | + }); 115 | + 116 | + // create new isolated world for this frame 117 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 118 | + frameId, 119 | + // use randomName for worldName to distinguish from normal utility world 120 | + worldName: randomName, 121 | + grantUniveralAccess: true, 122 | + }); 123 | + 124 | + // emit event in the specific frame from the isolated world 125 | + await client.send('Runtime.evaluate', { 126 | + expression: `document.dispatchEvent(new CustomEvent('${randomName}', { detail: { frameId: '${frameId}' } }))`, 127 | + contextId: createIsolatedWorldResult.executionContextId, 128 | + }); 129 | + } 130 | + 131 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getMainWorld] result:`, { contextId }); 132 | + return contextId; 133 | + } 134 | + async __re__getIsolatedWorld({ client, frameId, worldName }: any) { 135 | + const createIsolatedWorldResult = await client.send('Page.createIsolatedWorld', { 136 | + frameId, 137 | + worldName, 138 | + grantUniveralAccess: true, 139 | + }); 140 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][getIsolatedWorld] result:`, createIsolatedWorldResult); 141 | + return createIsolatedWorldResult.executionContextId; 142 | + } 143 | + // rebrowser-patches: get context id if it's missing 144 | + async acquireContextId(tryCount = 1): Promise { 145 | + if (this.#id > 0) { 146 | + return 147 | + } 148 | + 149 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding' 150 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] id = ${this.#id}, name = ${this.#name}, fixMode = ${fixMode}, tryCount = ${tryCount}`) 151 | + 152 | + let contextId: any 153 | + let tryAgain = true; 154 | + let errorMessage = 'N/A' 155 | + if (fixMode === 'addBinding') { 156 | + try { 157 | + if (this.#id === -2) { 158 | + // isolated world 159 | + contextId = await this.__re__getIsolatedWorld({ 160 | + client: this.#client, 161 | + frameId: this._frameId, 162 | + worldName: this.#name, 163 | + }) 164 | + } else { 165 | + // main world 166 | + contextId = await this.__re__getMainWorld({ 167 | + client: this.#client, 168 | + frameId: this._frameId, 169 | + isWorker: this.#id === -3, 170 | + }) 171 | + } 172 | + } catch (error: any) { 173 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.error('[rebrowser-patches][acquireContextId] error:', error) 174 | + errorMessage = error.message 175 | + if (error instanceof Error) { 176 | + if ( 177 | + error.message.includes('No frame for given id found') || 178 | + error.message.includes('Target closed') || 179 | + error.message.includes('Session closed') 180 | + ) { 181 | + // target doesn't exist anymore, don't try again 182 | + tryAgain = false 183 | + } 184 | + } 185 | + 186 | + debugError(error); 187 | + } 188 | + } else if (fixMode === 'alwaysIsolated') { 189 | + if (this.#id === -3) { 190 | + throw new Error('[rebrowser-patches] web workers are not supported in alwaysIsolated mode') 191 | + } 192 | + 193 | + contextId = await this.__re__getIsolatedWorld({ 194 | + client: this.#client, 195 | + frameId: this._frameId, 196 | + worldName: this.#name, 197 | + }) 198 | + } else if (fixMode === 'enableDisable') { 199 | + const executionContextCreatedHandler = ({ context }: any) => { 200 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][executionContextCreated] this.#id = ${this.#id}, name = ${this.#name}, contextId = ${contextId}, event.context.id = ${context.id}`) 201 | + 202 | + if (contextId > 0) { 203 | + // already acquired the id 204 | + return 205 | + } 206 | + 207 | + if (this.#id === -1) { 208 | + // main world 209 | + if (context.auxData && context.auxData['isDefault']) { 210 | + contextId = context.id 211 | + } 212 | + } else if (this.#id === -2) { 213 | + // utility world 214 | + if (this.#name === context.name) { 215 | + contextId = context.id 216 | + } 217 | + } else if (this.#id === -3) { 218 | + // web worker 219 | + contextId = context.id 220 | + } 221 | + } 222 | + 223 | + this.#client.on('Runtime.executionContextCreated', executionContextCreatedHandler) 224 | + await this.#client.send('Runtime.enable') 225 | + await this.#client.send('Runtime.disable') 226 | + this.#client.off('Runtime.executionContextCreated', executionContextCreatedHandler) 227 | + } 228 | + 229 | + if (!contextId) { 230 | + if (!tryAgain || tryCount >= 3) { 231 | + throw new Error(`[rebrowser-patches] acquireContextId failed (tryAgain = ${tryAgain}, tryCount = ${tryCount}), errorMessage: ${errorMessage}`) 232 | + } 233 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][acquireContextId] failed, try again (tryCount = ${tryCount})`) 234 | + return this.acquireContextId(tryCount + 1) 235 | + } 236 | + 237 | + this.#id = contextId 238 | + } 239 | + 240 | async #evaluate< 241 | Params extends unknown[], 242 | Func extends EvaluateFunc = EvaluateFunc, 243 | @@ -374,6 +561,13 @@ 244 | pageFunction: Func | string, 245 | ...args: Params 246 | ): Promise>> | Awaited>> { 247 | + // rebrowser-patches: context id is missing, acquire it and try again 248 | + if (this.#id < 0) { 249 | + await this.acquireContextId() 250 | + // @ts-ignore 251 | + return this.#evaluate(returnByValue, pageFunction, ...args) 252 | + } 253 | + 254 | const sourceUrlComment = getSourceUrlComment( 255 | getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ?? 256 | PuppeteerURL.INTERNAL_URL, 257 | 258 | --- a/src/cdp/FrameManager.ts 259 | +++ b/src/cdp/FrameManager.ts 260 | @@ -191,6 +191,10 @@ 261 | this.#onFrameStoppedLoading(event.frameId); 262 | }); 263 | session.on('Runtime.executionContextCreated', async event => { 264 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 265 | + // rebrowser-patches: ignore default logic 266 | + return 267 | + } 268 | await this.#frameTreeHandled?.valueOrThrow(); 269 | this.#onExecutionContextCreated(event.context, session); 270 | }); 271 | @@ -216,9 +220,17 @@ 272 | this.#frameTreeHandled?.resolve(); 273 | }), 274 | client.send('Page.setLifecycleEventsEnabled', {enabled: true}), 275 | - client.send('Runtime.enable').then(() => { 276 | - return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); 277 | - }), 278 | + (() => { 279 | + // rebrowser-patches: skip Runtime.enable 280 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 281 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][FrameManager] initialize') 282 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME) 283 | + } 284 | + 285 | + return client.send('Runtime.enable').then(() => { 286 | + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); 287 | + }) 288 | + })(), 289 | ...(frame 290 | ? Array.from(this.#scriptsToEvaluateOnNewDocument.values()) 291 | : [] 292 | @@ -229,6 +241,30 @@ 293 | return frame?.addExposedFunctionBinding(binding); 294 | }), 295 | ]); 296 | + 297 | + // rebrowser-patches: manually create main world context 298 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 299 | + this.frames() 300 | + .filter(frame => { 301 | + return frame.client === client; 302 | + }).map(frame => { 303 | + const world = frame.worlds[MAIN_WORLD] 304 | + const contextPayload = { 305 | + id: -1, 306 | + name: '', 307 | + auxData: { 308 | + frameId: frame._id, 309 | + } 310 | + } 311 | + const context = new ExecutionContext( 312 | + frame.client, 313 | + // @ts-ignore 314 | + contextPayload, 315 | + world 316 | + ); 317 | + world.setContext(context); 318 | + }) 319 | + } 320 | } catch (error) { 321 | this.#frameTreeHandled?.resolve(); 322 | // The target might have been closed before the initialization finished. 323 | @@ -455,6 +491,24 @@ 324 | this._frameTree.addFrame(frame); 325 | } 326 | 327 | + // rebrowser-patches: we cannot fully dispose contexts as they won't be recreated as we don't have Runtime events, 328 | + // instead, just mark it all empty 329 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 330 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches] onFrameNavigated, navigationType = ${navigationType}, id = ${framePayload.id}, url = ${framePayload.url}`) 331 | + for (const worldSymbol of [MAIN_WORLD, PUPPETEER_WORLD]) { 332 | + // @ts-ignore 333 | + if (frame?.worlds[worldSymbol].context) { 334 | + // @ts-ignore 335 | + const frameOrWorker = frame.worlds[worldSymbol].environment 336 | + if ('clearDocumentHandle' in frameOrWorker) { 337 | + frameOrWorker.clearDocumentHandle(); 338 | + } 339 | + // @ts-ignore 340 | + frame.worlds[worldSymbol].context?.clear(worldSymbol === MAIN_WORLD ? -1 : -2) 341 | + } 342 | + } 343 | + } 344 | + 345 | frame = await this._frameTree.waitForFrame(frameId); 346 | frame._navigated(framePayload); 347 | this.emit(FrameManagerEvent.FrameNavigated, frame); 348 | @@ -487,6 +541,24 @@ 349 | worldName: name, 350 | grantUniveralAccess: true, 351 | }) 352 | + .then((createIsolatedWorldResult: any) => { 353 | + // rebrowser-patches: save created context id 354 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] === '0') { 355 | + return 356 | + } 357 | + if (!createIsolatedWorldResult?.executionContextId) { 358 | + // probably "Target closed" error, just ignore it 359 | + return 360 | + } 361 | + // @ts-ignore 362 | + this.#onExecutionContextCreated({ 363 | + id: createIsolatedWorldResult.executionContextId, 364 | + name, 365 | + auxData: { 366 | + frameId: frame._id, 367 | + } 368 | + }, frame.client) 369 | + }) 370 | .catch(debugError); 371 | }), 372 | ); 373 | 374 | --- a/src/cdp/IsolatedWorld.ts 375 | +++ b/src/cdp/IsolatedWorld.ts 376 | @@ -1,3 +1,4 @@ 377 | +//@ts-nocheck 378 | /** 379 | * @license 380 | * Copyright 2019 Google Inc. 381 | @@ -18,13 +19,14 @@ 382 | fromEmitterEvent, 383 | timeout, 384 | withSourcePuppeteerURLIfNone, 385 | + UTILITY_WORLD_NAME, 386 | } from '../common/util.js'; 387 | import {disposeSymbol} from '../util/disposable.js'; 388 | 389 | import {CdpElementHandle} from './ElementHandle.js'; 390 | -import type {ExecutionContext} from './ExecutionContext.js'; 391 | +import {ExecutionContext} from './ExecutionContext.js'; 392 | import type {CdpFrame} from './Frame.js'; 393 | -import type {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; 394 | +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; 395 | import {CdpJSHandle} from './JSHandle.js'; 396 | import type {CdpWebWorker} from './WebWorker.js'; 397 | 398 | @@ -137,6 +139,23 @@ 399 | * Waits for the next context to be set on the isolated world. 400 | */ 401 | async #waitForExecutionContext(): Promise { 402 | + const fixMode = process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] || 'addBinding'; 403 | + if (fixMode === 'addBinding') { 404 | + const isMainWorld = this.#frameOrWorker.worlds[MAIN_WORLD] === this; 405 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log(`[rebrowser-patches][waitForExecutionContext] frameId = ${this.#frameOrWorker._id}, isMainWorld = ${isMainWorld}`); 406 | + 407 | + const contextPayload = { 408 | + id: isMainWorld ? -1 : -2, 409 | + name: isMainWorld ? '' : UTILITY_WORLD_NAME, 410 | + auxData: { 411 | + frameId: this.#frameOrWorker._id, 412 | + } 413 | + }; 414 | + const context = new ExecutionContext(this.client, contextPayload, this); 415 | + this.setContext(context); 416 | + return context; 417 | + } 418 | + 419 | const error = new Error('Execution context was destroyed'); 420 | const result = await firstValueFrom( 421 | fromEmitterEvent(this.#emitter, 'context').pipe( 422 | @@ -206,6 +225,8 @@ 423 | if (!context) { 424 | context = await this.#waitForExecutionContext(); 425 | } 426 | + // rebrowser-patches: make sure id is acquired 427 | + await context.acquireContextId() 428 | const {object} = await this.client.send('DOM.resolveNode', { 429 | backendNodeId: backendNodeId, 430 | executionContextId: context.id, 431 | 432 | --- a/src/cdp/WebWorker.ts 433 | +++ b/src/cdp/WebWorker.ts 434 | @@ -58,6 +58,10 @@ 435 | this.#world = new IsolatedWorld(this, new TimeoutSettings()); 436 | 437 | this.#client.once('Runtime.executionContextCreated', async event => { 438 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 439 | + // rebrowser-patches: ignore default logic 440 | + return 441 | + } 442 | this.#world.setContext( 443 | new ExecutionContext(client, event.context, this.#world), 444 | ); 445 | @@ -82,7 +86,22 @@ 446 | 447 | // This might fail if the target is closed before we receive all execution contexts. 448 | networkManager?.addClient(this.#client).catch(debugError); 449 | - this.#client.send('Runtime.enable').catch(debugError); 450 | + if (process.env['REBROWSER_PATCHES_RUNTIME_FIX_MODE'] !== '0') { 451 | + // @ts-ignore 452 | + process.env['REBROWSER_PATCHES_DEBUG'] && console.log('[rebrowser-patches][WebWorker] initialize', targetType, targetId, client._target(), client._target()._getTargetInfo()) 453 | + 454 | + // rebrowser-patches: manually create context 455 | + const contextPayload = { 456 | + id: -3, 457 | + auxData: { 458 | + frameId: targetId, 459 | + } 460 | + } 461 | + // @ts-ignore 462 | + this.#world.setContext(new ExecutionContext(client, contextPayload, this.#world)); 463 | + } else { 464 | + this.#client.send('Runtime.enable').catch(debugError); 465 | + } 466 | } 467 | 468 | mainRealm(): Realm { 469 | 470 | --- a/src/common/util.ts 471 | +++ b/src/common/util.ts 472 | @@ -299,7 +299,9 @@ 473 | * @internal 474 | */ 475 | export const UTILITY_WORLD_NAME = 476 | - '__puppeteer_utility_world__' + packageVersion; 477 | + // rebrowser-patches: change utility world name 478 | + process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] !== '0' ? (process.env['REBROWSER_PATCHES_UTILITY_WORLD_NAME'] || 'util') : 479 | + '__puppeteer_utility_world__' + packageVersion; 480 | 481 | /** 482 | * @internal 483 | @@ -310,6 +312,10 @@ 484 | * @internal 485 | */ 486 | export function getSourceUrlComment(url: string): string { 487 | + // rebrowser-patches: change sourceUrl to generic script name 488 | + if (process.env['REBROWSER_PATCHES_SOURCE_URL'] !== '0') { 489 | + url = process.env['REBROWSER_PATCHES_SOURCE_URL'] || 'app.js' 490 | + } 491 | return `//# sourceURL=${url}`; 492 | } 493 | 494 | -------------------------------------------------------------------------------- /scripts/patcher.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { readFile } from 'node:fs/promises' 4 | import { resolve } from 'node:path' 5 | 6 | import yargs from 'yargs' 7 | import { hideBin } from 'yargs/helpers' 8 | 9 | import { 10 | exec, 11 | fatalError, 12 | getPatchBaseCmd, 13 | getPatcherPackagePath, 14 | log, 15 | validPackagesNames, 16 | } from './utils/index.js'; 17 | 18 | (async () => { 19 | // config and preparations 20 | const cliArgs = yargs(hideBin(process.argv)) 21 | .usage('Usage: [options]') 22 | .command('patch', 'Apply patch') 23 | .command('unpatch', 'Reverse patch') 24 | .command('check', 'Check if patch is already applied') 25 | .describe('packageName', `Target package name: ${validPackagesNames.join(', ')}`) 26 | .describe('packagePath', 'Path to the target package') 27 | .describe('codeTarget', 'What code to patch: src or lib') 28 | .boolean('debug') 29 | .describe('debug', 'Enable debugging mode') 30 | .demandCommand(1, 1, 'Error: choose a command (patch, unpatch, check)') 31 | .parse() 32 | 33 | let { 34 | packageName, 35 | packagePath, 36 | codeTarget = 'lib', 37 | debug, 38 | } = cliArgs 39 | 40 | if (debug) { 41 | process.env.REBROWSER_PATCHES_DEBUG = 1 42 | } 43 | 44 | const command = cliArgs._[0] 45 | let commandResult 46 | 47 | if (!packagePath && !packageName) { 48 | fatalError('You need to specify either packageName or packagePath.') 49 | } 50 | 51 | if (!packagePath) { 52 | packagePath = `${process.cwd()}/node_modules/${packageName}` 53 | } 54 | 55 | if (!['patch', 'unpatch', 'check'].includes(command)) { 56 | fatalError(`Unknown command: ${command}`) 57 | } 58 | 59 | if (!['src', 'lib'].includes(codeTarget)) { 60 | fatalError(`Unknown codeTarget: ${codeTarget}`) 61 | } 62 | 63 | log('Config:') 64 | log(`command = ${command}, packageName = ${packageName}, codeTarget = ${codeTarget}`) 65 | log(`packagePath = ${packagePath}`) 66 | log('------') 67 | 68 | // find package 69 | let packageJson 70 | const packageJsonPath = resolve(packagePath, 'package.json') 71 | try { 72 | const packageJsonText = await readFile(packageJsonPath, { encoding: 'utf8' }) 73 | packageJson = JSON.parse(packageJsonText) 74 | } catch (err) { 75 | fatalError('Cannot read package.json', err) 76 | } 77 | if (!packageName) { 78 | if (!validPackagesNames.includes(packageJson.name)) { 79 | fatalError(`Package name is "${packageJson.name}", but we only support ${validPackagesNames.join(', ')}.`) 80 | } else { 81 | packageName = packageJson.name 82 | } 83 | } else if (packageJson.name !== packageName) { 84 | fatalError(`Package name is "${packageJson.name}", but we're looking for "${packageName}". Check your package path.`) 85 | } 86 | log(`Found package "${packageJson.name}", version ${packageJson.version}`) 87 | 88 | const patchFilePath = resolve(getPatcherPackagePath(), `./patches/${packageName}/${codeTarget}.patch`) 89 | 90 | // check patch status 91 | let patchStatus 92 | try { 93 | const { stdout, stderr } = await exec(`${getPatchBaseCmd(patchFilePath)} --dry-run`, { 94 | cwd: packagePath, 95 | }) 96 | patchStatus = 'unpatched' 97 | } catch (e) { 98 | if (e.stdout.includes('No file to patch')) { 99 | fatalError('Internal error, patch command cannot find file to patch') 100 | } else if (e.stdout.includes('Ignoring previously applied (or reversed) patch')) { 101 | patchStatus = 'patched' 102 | } else if (e.stderr.includes('is not recognized')) { 103 | let message = 'patch command not found!' 104 | if (process.platform === 'win32') { 105 | message += '\nCheck README for how to install patch.exe on Windows.' 106 | } 107 | fatalError(message) 108 | } else { 109 | log('[debug] patch error:', e) 110 | throw e 111 | } 112 | } 113 | log(`Current patch status = ${patchStatus === 'patched' ? '🟩' : '🟧'} ${patchStatus}`) 114 | 115 | // run command 116 | let execCmd 117 | if (command === 'patch') { 118 | if (patchStatus === 'patched') { 119 | log('Package already patched.') 120 | } else { 121 | execCmd = getPatchBaseCmd(patchFilePath) 122 | } 123 | } else if (command === 'unpatch') { 124 | if (patchStatus === 'unpatched') { 125 | log('Package already unpatched.') 126 | } else { 127 | execCmd = `${getPatchBaseCmd(patchFilePath)} --reverse` 128 | } 129 | } 130 | 131 | if (execCmd) { 132 | try { 133 | const { stdout, stderr } = await exec(execCmd, { 134 | cwd: packagePath, 135 | }) 136 | commandResult = 'success' 137 | } catch (e) { 138 | log('patch exec error:', e) 139 | commandResult = 'error' 140 | } 141 | } 142 | 143 | // process results 144 | let exitCode = 0 145 | let resultText 146 | if (!commandResult) { 147 | resultText = '🟡 nothing changed' 148 | } else if (commandResult === 'success') { 149 | resultText = '🟢 success' 150 | } else if (commandResult === 'error') { 151 | resultText = '🔴 error' 152 | exitCode = 1 153 | } 154 | log(`Result: ${resultText}`) 155 | 156 | process.exit(exitCode) 157 | })() 158 | -------------------------------------------------------------------------------- /scripts/utils/index.js: -------------------------------------------------------------------------------- 1 | import { exec as execNative } from 'node:child_process' 2 | import { dirname, resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { promisify } from 'node:util' 5 | 6 | const promisifiedExec = promisify(execNative) 7 | 8 | export const validPackagesNames = ['puppeteer-core', 'playwright-core'] 9 | 10 | export const exec = async (...args) => { 11 | if (isDebug()) { 12 | log('[debug][exec]', args) 13 | } 14 | 15 | const execRes = await promisifiedExec(...args) 16 | 17 | if (isDebug()) { 18 | log('[debug][execRes]', execRes) 19 | } 20 | 21 | return execRes 22 | } 23 | export const log = console.log 24 | 25 | export const getPatcherPackagePath = () => { 26 | return resolve(dirname(fileURLToPath(import.meta.url)), '../..') 27 | } 28 | 29 | export const fatalError = (...args) => { 30 | console.error('❌ FATAL ERROR:', ...args) 31 | process.exit(1) 32 | } 33 | 34 | export const getPatchBaseCmd = (patchFilePath) => { 35 | return `patch --batch -p1 --input=${patchFilePath} --verbose --no-backup-if-mismatch --reject-file=- --forward --silent` 36 | } 37 | 38 | export const isDebug = () => { 39 | return !!process.env.REBROWSER_PATCHES_DEBUG 40 | } 41 | --------------------------------------------------------------------------------