@mefistofelix/cdp.js 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -80
- package/cdp.js +72 -24
- package/cdp_ext/cdp_ext.js +110 -8
- package/cdp_ext/manifest.json +1 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ import api, { util, jsrpc, browser, cdp } from '@mefistofelix/cdp.js'
|
|
|
25
25
|
The npm package exports the same file and API. Direct file imports in the examples
|
|
26
26
|
below remain supported. Publication metadata is isolated in `npm/`.
|
|
27
27
|
The package includes both `cdp_ext/` files. With `cdp_ext: true`, the manager
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
prepares and loads the extension automatically with a custom user-data directory.
|
|
29
|
+
The system profile uses an already installed extension by default. Reverse mode still
|
|
30
30
|
requires Deno or Bun. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
31
31
|
In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above.
|
|
32
32
|
|
|
@@ -189,7 +189,7 @@ belong in `params.target` and do not inherit from the constructor.
|
|
|
189
189
|
|
|
190
190
|
For a new browser record, configuration is assembled in this order:
|
|
191
191
|
|
|
192
|
-
1. Default `user_data_dir
|
|
192
|
+
1. Default `user_data_dir`: unset in extension/passive reverse mode, otherwise derived from `base_path` and the browser name.
|
|
193
193
|
2. Constructor browser options.
|
|
194
194
|
3. The `params.browser` configuration object.
|
|
195
195
|
|
|
@@ -225,7 +225,7 @@ local configuration.
|
|
|
225
225
|
| `base_path` | string, `'.cdp'` | Constructor-only profile root, resolved against the working directory. |
|
|
226
226
|
| `reverse_server` | object, `{}` | Constructor-only settings for the shared lazy listener: `hostname` defaults to `'127.0.0.1'`, `port` to `9223`. Independent of browser attachment `host`/`port`. |
|
|
227
227
|
| `name` | string, `'main'` when routing is omitted | Required in a browser configuration object; dictionary key and default profile directory label. |
|
|
228
|
-
| `user_data_dir` | absolute path string or `null`, `<base_path>/<name>` | Chromium user-data directory containing profiles such as `Default`. Explicit paths should be absolute. `null` omits the switch and skips manager preference-file writes; Chromium selects its system directory. |
|
|
228
|
+
| `user_data_dir` | absolute path string or `null`, `null` in extension/passive reverse mode, otherwise `<base_path>/<name>` | Chromium user-data directory containing profiles such as `Default`. Explicit paths should be absolute. `null` omits the switch and skips manager preference-file writes; Chromium selects its system directory. |
|
|
229
229
|
| `executable_path` | string, automatic discovery | Executable to spawn; takes precedence over `CDP_BROWSER`. |
|
|
230
230
|
| `headless` | boolean, `false` | When true, add `headless: 'new'` and default `window-size: '1440,900'`. |
|
|
231
231
|
| `windowsHide` | boolean, effective headless mode | Hide the spawned process's console on Windows. Defaults to true when the final arguments include `--headless`, false otherwise; an explicit value wins. Does not hide the parent terminal. |
|
|
@@ -242,9 +242,10 @@ local configuration.
|
|
|
242
242
|
| `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
|
|
243
243
|
| `waitForDebuggerOnStart` | boolean, `false` | Ask Chromium to pause newly auto-attached targets. Applied to `Target.setAutoAttach` when connecting; not a CLI argument or target option. |
|
|
244
244
|
| `websocket_url` | string, unset | Attach directly to a browser WebSocket endpoint. |
|
|
245
|
-
| `cdp_ext` | boolean, `false` |
|
|
245
|
+
| `cdp_ext` | boolean, `false` | Use the extension transport under Deno/Bun: start the shared reverse listener and open a local bootstrap URL containing the logical browser name and WebSocket endpoint. No extension ID is required. |
|
|
246
|
+
| `cdp_ext_install` | boolean, presence of an effective user-data directory | Prepare an extension copy and startup loading arguments. Defaults on for an explicit custom directory, off for the system profile. `false` uses an already installed extension. Only applies with `cdp_ext: true`. |
|
|
246
247
|
| `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno or Bun. |
|
|
247
|
-
| `extension_id` | string, browser name for
|
|
248
|
+
| `extension_id` | string, browser name for passive `reverse: true` attachment | Optional legacy incoming extension identity. Not needed with `cdp_ext: true`; observed automatically from the connection. |
|
|
248
249
|
| `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
|
|
249
250
|
| `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
|
|
250
251
|
| `host` | string, `'127.0.0.1'` | Host used with `port`. |
|
|
@@ -492,8 +493,8 @@ can configure its native incognito scope. See
|
|
|
492
493
|
[cdp_ext/manifest.json](cdp_ext/manifest.json) and
|
|
493
494
|
[cdp_ext/cdp_ext.js](cdp_ext/cdp_ext.js) form a Manifest V3 extension for Chrome
|
|
494
495
|
125+. The extension connects out to one WebSocket server. The server serves all
|
|
495
|
-
browser instances on the same port;
|
|
496
|
-
|
|
496
|
+
browser instances on the same port; bootstrapped extensions announce the logical
|
|
497
|
+
browser name and native extension ID. No remote-debugging argument is needed.
|
|
497
498
|
|
|
498
499
|
Run the server under Deno using its native
|
|
499
500
|
[WebSocket upgrade](https://docs.deno.com/api/deno/websockets/) or under Bun using
|
|
@@ -513,6 +514,7 @@ const result = await client.call({
|
|
|
513
514
|
browser: {
|
|
514
515
|
name: 'work',
|
|
515
516
|
cdp_ext: true,
|
|
517
|
+
user_data_dir: '/absolute/path/to/work-profile',
|
|
516
518
|
headless: true,
|
|
517
519
|
},
|
|
518
520
|
target: 'demo',
|
|
@@ -521,65 +523,98 @@ const result = await client.call({
|
|
|
521
523
|
})
|
|
522
524
|
```
|
|
523
525
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
`update_preferences` rules.
|
|
530
|
-
The generated launch arguments omit remote debugging and enable this extension.
|
|
531
|
-
An `extensions` array also selects those directories; `extensions: true` keeps
|
|
532
|
-
normally enabled profile extensions through `'*'`. Raw `args` still win and can
|
|
533
|
-
prevent the bridge from loading or connecting, causing a connection timeout.
|
|
534
|
-
|
|
535
|
-
For external attachment, load unpacked `cdp_ext/` through `chrome://extensions`
|
|
536
|
-
or the [local-extension argument options](#local-extensions-at-startup).
|
|
537
|
-
Use its ID shown in Chrome, or `browser.extension_id(directory)`, as the
|
|
538
|
-
browser option `extension_id`. The logical `name` stays in the manager.
|
|
539
|
-
|
|
540
|
-
The extension configuration is read from `chrome.runtime.getManifest()`:
|
|
541
|
-
|
|
542
|
-
| Manifest `cdp` key | Default | Meaning |
|
|
543
|
-
| --- | --- | --- |
|
|
544
|
-
| `websocket_url` | `ws://127.0.0.1:9223` | The shared server endpoint. Port 9223 avoids the usual direct CDP port 9222. |
|
|
545
|
-
| `reconnect_ms` | `5000` | Connection retry interval while the extension worker is awake; also the empty-message heartbeat interval while connected. Use a positive value below 30000. |
|
|
546
|
-
|
|
547
|
-
The first message uses `chrome.runtime.id`, not a configured logical browser name.
|
|
548
|
-
For an unpacked extension without a manifest `key`, Chromium derives this ID from
|
|
549
|
-
its directory. Managed launches use a separate directory for each profile, so
|
|
550
|
-
the ID is stable across reconnects and restarts, and the manager knows which
|
|
551
|
-
browser it belongs to. External instances must likewise use distinct extension
|
|
552
|
-
directories. Moving the directory changes the ID. The server rejects duplicate
|
|
553
|
-
live IDs and announcements that differ from the WebSocket's `chrome-extension://`
|
|
554
|
-
origin. This is routing information, not an authentication credential.
|
|
526
|
+
With just `cdp_ext: true`, Chrome uses its system user-data directory. No
|
|
527
|
+
`user-data-dir` switch is added. An already installed and enabled bridge reads
|
|
528
|
+
the local URL and connects to the waiting manager. The user enters no extension
|
|
529
|
+
ID or WebSocket address. Load the package's `cdp_ext/` once through
|
|
530
|
+
`chrome://extensions` (developer mode, Load unpacked); reload it after upgrading.
|
|
555
531
|
|
|
556
532
|
```js
|
|
557
|
-
import { browser, cdp } from './cdp.js'
|
|
558
|
-
|
|
559
533
|
const client = new cdp({
|
|
560
|
-
|
|
561
|
-
connect_timeout_ms: 60000,
|
|
562
|
-
reverse_server: {
|
|
563
|
-
hostname: '127.0.0.1',
|
|
564
|
-
port: 9223,
|
|
565
|
-
},
|
|
534
|
+
cdp_ext: true,
|
|
566
535
|
})
|
|
567
|
-
|
|
568
|
-
method: '
|
|
569
|
-
params: {
|
|
570
|
-
browser: {
|
|
571
|
-
name: 'other',
|
|
572
|
-
reverse: true,
|
|
573
|
-
extension_id: browser.extension_id('/absolute/path/to/cdp_ext'),
|
|
574
|
-
},
|
|
575
|
-
target: 'demo',
|
|
576
|
-
expression: 'document.title',
|
|
577
|
-
},
|
|
536
|
+
await client.call({
|
|
537
|
+
method: 'Browser.getVersion',
|
|
538
|
+
params: { browser: 'work' },
|
|
578
539
|
})
|
|
579
|
-
console.log(result.result.value)
|
|
580
|
-
await client.close()
|
|
581
540
|
```
|
|
582
541
|
|
|
542
|
+
Provide `user_data_dir` or raw `args['user-data-dir']` to choose a custom
|
|
543
|
+
directory. An effective custom directory makes `cdp_ext_install` default to true:
|
|
544
|
+
the manager copies the two extension files into
|
|
545
|
+
`<user-data-dir>/<profile-directory>/cdp_ext/` (`Default` when unset) and adds
|
|
546
|
+
startup loading flags. The server address comes only from the trigger URL. Normal launch
|
|
547
|
+
options and profile preferences then apply. Set `cdp_ext_install: false` to use
|
|
548
|
+
an extension already installed in that profile, or true to force preparation and
|
|
549
|
+
loading. A null/false raw directory override makes automatic installation default off.
|
|
550
|
+
|
|
551
|
+
Forced installation without a directory places only the extension copy under
|
|
552
|
+
`<base_path>/<name>/<profile-directory>/cdp_ext/`; Chrome still selects its system
|
|
553
|
+
profile. The manager never infers system profile paths for preference writes.
|
|
554
|
+
Loading switches apply only at Chrome startup. An already running Chrome cannot
|
|
555
|
+
load a new extension from a forwarded command line: install it once or restart
|
|
556
|
+
that Chrome before forcing installation.
|
|
557
|
+
|
|
558
|
+
With installation disabled, opening the link skips the automation launch preset
|
|
559
|
+
so normal Chrome can forward the URL into its existing instance. High-level
|
|
560
|
+
headless, proxy, image, translation, login and extension options still generate
|
|
561
|
+
their flags, followed by raw overrides and the same enabled-only wildcard
|
|
562
|
+
expansion. The manager does not claim ownership of Chrome. Preference writes are
|
|
563
|
+
independent of extension installation: `update_preferences` controls them and
|
|
564
|
+
they require an explicit directory. A running instance does not apply new flags.
|
|
565
|
+
With installation
|
|
566
|
+
enabled, the normal launch preset applies, generated flags omit remote debugging
|
|
567
|
+
and enable the bridge. An `extensions` array adds its directories;
|
|
568
|
+
`extensions: true` retains enabled profile extensions through `'*'`. Raw `args`
|
|
569
|
+
still win and can prevent loading or connection.
|
|
570
|
+
|
|
571
|
+
Both launch paths keep `--no-first-run` and
|
|
572
|
+
`--disable-search-engine-choice-screen`, including when opening a new custom
|
|
573
|
+
profile with installation disabled. This suppresses onboarding and the search
|
|
574
|
+
engine selection prompt without changing the chosen search provider or writing
|
|
575
|
+
search preferences. Raw `args` can override either flag. An existing Chrome only
|
|
576
|
+
receives the URL; new startup flags require restarting that instance.
|
|
577
|
+
|
|
578
|
+
The manager opens an actual local HTTP URL on the shared listener, for example
|
|
579
|
+
`http://127.0.0.1:9223/cdp_ext?browser=work&websocket_url=...&token=...`.
|
|
580
|
+
The port is the effective listening port, including an automatically allocated
|
|
581
|
+
port for `reverse_server.port: 0`. Name, WebSocket URL and generated binding token
|
|
582
|
+
are internal bootstrap parameters, not additional user options. The extension
|
|
583
|
+
observes startup tabs, tab creation and URL changes. Bootstrap accepts only
|
|
584
|
+
loopback HTTP URLs with a WebSocket endpoint on the same host/port. The bootstrap
|
|
585
|
+
tab remains open.
|
|
586
|
+
|
|
587
|
+
The first announcement contains `{ browser, extension_id, token }`. The browser
|
|
588
|
+
field is the logical label; `extension_id` is `chrome.runtime.id` and must match
|
|
589
|
+
the upgrade origin. The token must match the pending manager record. One extension
|
|
590
|
+
directory and native ID can therefore serve multiple profiles with distinct
|
|
591
|
+
logical labels. No logical name is written into a manifest. Bootstrap settings
|
|
592
|
+
use `chrome.storage.session` to survive worker restarts; they disappear on browser
|
|
593
|
+
restart or extension reload. Manager browser/target/session mappings remain in memory.
|
|
594
|
+
|
|
595
|
+
A profile has one active logical connection. A link requesting another name or
|
|
596
|
+
manager while connected returns an explicit error, preserving the existing
|
|
597
|
+
connection. After disconnection, a new bootstrap can rebind it.
|
|
598
|
+
|
|
599
|
+
Passive external attachment remains available through `reverse: true`: it opens
|
|
600
|
+
no URL and waits for `extension_id` (or the browser label). This compatibility
|
|
601
|
+
mode is optional; `cdp_ext: true` needs neither `reverse` nor an ID.
|
|
602
|
+
The current extension still requires a trigger URL in passive mode; it never
|
|
603
|
+
connects automatically to a manifest endpoint or a default port.
|
|
604
|
+
|
|
605
|
+
The extension stays idle until a valid trigger URL supplies the logical name,
|
|
606
|
+
WebSocket endpoint and token. Before that it makes no server requests and logs
|
|
607
|
+
no connection messages. A worker restart may resume the same browser session's
|
|
608
|
+
previous trigger from `chrome.storage.session`.
|
|
609
|
+
|
|
610
|
+
The shipped manifest needs no custom configuration keys. An optional custom
|
|
611
|
+
`cdp.reconnect_ms` override remains supported (Chrome displays a harmless
|
|
612
|
+
unrecognized-key warning if it is added):
|
|
613
|
+
|
|
614
|
+
| Manifest `cdp` key | Default | Meaning |
|
|
615
|
+
| --- | --- | --- |
|
|
616
|
+
| `reconnect_ms` | `5000` | Awake retry and connected heartbeat interval; use a positive value below 30000. |
|
|
617
|
+
|
|
583
618
|
Construction and direct CDP calls do not start a listener. The first browser call
|
|
584
619
|
using `reverse: true` or `cdp_ext: true` starts the shared server lazily, with the
|
|
585
620
|
constructor's `reverse_server` settings. To mix direct and reverse browsers, leave the
|
|
@@ -616,7 +651,8 @@ Native Bun WebSocket limits still apply.
|
|
|
616
651
|
Reverse calls reuse the normal target setup, custom handlers, mappings, routed
|
|
617
652
|
events, request correlation, timeout and error behavior. `reverse: true` alone
|
|
618
653
|
attaches externally: the manager neither launches nor updates that browser.
|
|
619
|
-
`
|
|
654
|
+
`cdp_ext_install: true` owns the launched process and applies normal launch preferences.
|
|
655
|
+
With installation off, Chrome is external and remains open on manager close.
|
|
620
656
|
Closing the manager closes owned processes and the server, while external
|
|
621
657
|
browsers and tabs remain open. The extension retries when the server returns;
|
|
622
658
|
the next call rebuilds discovery and sessions, preserving surviving target IDs.
|
|
@@ -628,20 +664,11 @@ for that extension to reconnect; they do not spawn another process. Only the
|
|
|
628
664
|
child process's confirmed exit permits a new launch. Explicit manager `close()`
|
|
629
665
|
still terminates owned browsers.
|
|
630
666
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
Chrome's [process singleton](https://github.com/chromium/chromium/blob/main/chrome/browser/process_singleton.h)
|
|
638
|
-
uses the user-data directory to decide whether to forward a new launch to an
|
|
639
|
-
existing instance. `no-default-browser-check` only suppresses the default-browser
|
|
640
|
-
prompt. The current Chromium [notification callback](https://github.com/chromium/chromium/blob/main/chrome/browser/chrome_browser_main.cc)
|
|
641
|
-
rejects forwarding when either command line enables automation or uses headless
|
|
642
|
-
mode. This library generates `enable-automation` by default; a normal browser
|
|
643
|
-
started with `args['enable-automation']: false` can behave differently. Do not
|
|
644
|
-
infer ownership of the real browser from a short-lived forwarding process.
|
|
667
|
+
Chrome selects an existing instance by user-data directory. The minimal link
|
|
668
|
+
opener preserves this normal behavior. Automation/headless flags can prevent
|
|
669
|
+
forwarding; `no-default-browser-check` only suppresses a prompt. The short-lived
|
|
670
|
+
launcher is `record.opener`; its exit is not proof of browser exit. Installed
|
|
671
|
+
launches use `record.proc` instead.
|
|
645
672
|
|
|
646
673
|
The [service-worker lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle)
|
|
647
674
|
allows termination while offline. A 30-second Chrome alarm wakes the worker for
|
|
@@ -649,10 +676,20 @@ another connection attempt; the configured interval applies while it is awake.
|
|
|
649
676
|
Connected empty-text heartbeats keep an otherwise idle WebSocket worker active.
|
|
650
677
|
These are transport messages, not event buffering or polling of browser state.
|
|
651
678
|
|
|
679
|
+
After a trigger, the server may be offline normally. Before each disconnected
|
|
680
|
+
WebSocket attempt the extension checks the same server's HTTP `/cdp_ext` endpoint
|
|
681
|
+
with a two-second timeout and no cache. It opens the socket only when that
|
|
682
|
+
endpoint identifies CDP.js. Concurrent retries share the check; an unavailable or
|
|
683
|
+
incompatible server causes one informational waiting message, not a failing
|
|
684
|
+
WebSocket attempt on every timer tick. Successful connections and later
|
|
685
|
+
disconnects log their status once per transition. Chrome can still emit a native
|
|
686
|
+
network diagnostic if the server disappears between the HTTP check and upgrade.
|
|
687
|
+
|
|
652
688
|
### Reverse protocol and supported commands
|
|
653
689
|
|
|
654
|
-
The
|
|
655
|
-
|
|
690
|
+
The current extension's first announcement contains `{ browser, extension_id, token }`
|
|
691
|
+
after a trigger. The server also accepts legacy native-ID announcements.
|
|
692
|
+
Subsequent messages use CDP envelopes:
|
|
656
693
|
|
|
657
694
|
- Request: `{ id, method, params, sessionId? }`.
|
|
658
695
|
- Response: `{ id, result }` or `{ id, error: { code, message } }`.
|
|
@@ -1618,8 +1655,8 @@ dictionary containers have a null prototype.
|
|
|
1618
1655
|
| `client.reverse_server` | Shared listener configuration; the effective hostname/port are stored after startup. |
|
|
1619
1656
|
| `client.custom_methods[name]` | Handler function. |
|
|
1620
1657
|
| `client.browsers[name]` | Browser configuration fields plus live state. |
|
|
1621
|
-
| `client.reverse_sockets[
|
|
1622
|
-
| `record.extension_id`, `record.extension_path` |
|
|
1658
|
+
| `client.reverse_sockets[browser_name]` | Announced socket (native extension ID for passive attachment); native on Deno, an EventTarget adapter on Bun. |
|
|
1659
|
+
| `record.extension_id`, `record.extension_path` | Observed native extension identity and, when installed by the manager, the copy's canonical directory. |
|
|
1623
1660
|
| `client.server` | Current native Deno/Bun reverse server with shared lifecycle fields, or unset/null when not listening. |
|
|
1624
1661
|
| `client.server_closing` | Shutdown promise while the reverse listener is stopping, otherwise unset/null. |
|
|
1625
1662
|
| `record.socket` | Current `jsrpc` connection once created. |
|
|
@@ -1677,7 +1714,10 @@ try {
|
|
|
1677
1714
|
`browser.launch` builds arguments, starts the process, reads the
|
|
1678
1715
|
DevTools WebSocket URL from stderr and returns `{ proc, url }`. With `cdp_ext: true`,
|
|
1679
1716
|
it waits for process spawn instead and returns `url: undefined`; the caller must
|
|
1680
|
-
prepare the extension and supply its directory as `extension_path
|
|
1717
|
+
prepare the extension and supply its directory as `extension_path` when loading
|
|
1718
|
+
it. With `cdp_ext_install: false`, the helper skips the automation preset, uses
|
|
1719
|
+
the same option/argument/wildcard handling, and returns an unreferenced launcher.
|
|
1720
|
+
`bootstrap_url` is the optional startup URL, defaulting to `about:blank`.
|
|
1681
1721
|
It does not open a CDP connection or write preference files; `update_preferences`, `preferences`
|
|
1682
1722
|
and `local_state` are manager options, ignored by this helper.
|
|
1683
1723
|
To prepare files independently, call
|
|
@@ -1784,5 +1824,16 @@ Managed-launch coverage checks concurrent first use, profile-specific identity,
|
|
|
1784
1824
|
connection timeout without process replacement, lazy listener startup/shutdown
|
|
1785
1825
|
independent of direct browsers, and relaunch after confirmed exit. External reverse
|
|
1786
1826
|
timeouts release the listening port; other live reverse connections retain it.
|
|
1827
|
+
URL bootstrap also uses disposable normal Chrome profiles to check reuse of an
|
|
1828
|
+
already running instance, startup when absent, one shared extension directory,
|
|
1829
|
+
logical names, binding tokens, conflicting names and preservation on manager close.
|
|
1830
|
+
Installation defaults, raw directory overrides, profile writes independent of
|
|
1831
|
+
installation and shared wildcard expansion are covered without opening personal profiles.
|
|
1832
|
+
Argument coverage crosses direct/extension launch modes with headless, proxy,
|
|
1833
|
+
image/login/translation options, extension selectors and raw overrides. Profile
|
|
1834
|
+
combinations include default/custom directories, null/false raw directory
|
|
1835
|
+
overrides, forced installation and independent preference-write controls.
|
|
1836
|
+
Offline worker checks verify silence before a trigger, deduplicated waiting logs,
|
|
1837
|
+
no WebSocket attempts against absent/incompatible servers and concurrent retry checks.
|
|
1787
1838
|
The npm tarball contains `package.json`, `cdp.js`, `README.md` and the two extension
|
|
1788
1839
|
files required by managed reverse launches.
|
package/cdp.js
CHANGED
|
@@ -125,7 +125,10 @@ export class browser {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
static build_args(options = {}) {
|
|
128
|
-
const args = {
|
|
128
|
+
const args = options.cdp_ext && options.cdp_ext_install === false ? {
|
|
129
|
+
'user-data-dir': options.user_data_dir,
|
|
130
|
+
'disable-features': [],
|
|
131
|
+
} : {
|
|
129
132
|
'enable-automation': true,
|
|
130
133
|
'mute-audio': true,
|
|
131
134
|
'disable-blink-features': 'AutomationControlled',
|
|
@@ -150,12 +153,10 @@ export class browser {
|
|
|
150
153
|
'disable-prompt-on-repost': true,
|
|
151
154
|
'force-color-profile': 'srgb',
|
|
152
155
|
'metrics-recording-only': true,
|
|
153
|
-
'no-first-run': true,
|
|
154
156
|
'password-store': 'basic',
|
|
155
157
|
'use-mock-keychain': true,
|
|
156
158
|
'no-service-autorun': true,
|
|
157
159
|
'export-tagged-pdf': true,
|
|
158
|
-
'disable-search-engine-choice-screen': true,
|
|
159
160
|
'disable-features': [
|
|
160
161
|
'InfiniteSessionRestore',
|
|
161
162
|
'ImprovedCookieControls',
|
|
@@ -174,6 +175,8 @@ export class browser {
|
|
|
174
175
|
'user-data-dir': options.user_data_dir,
|
|
175
176
|
'remote-debugging-port': 0,
|
|
176
177
|
}
|
|
178
|
+
args['no-first-run'] = true
|
|
179
|
+
args['disable-search-engine-choice-screen'] = true
|
|
177
180
|
if (options.headless) {
|
|
178
181
|
args.headless = 'new'
|
|
179
182
|
args['window-size'] = '1440,900'
|
|
@@ -264,10 +267,17 @@ export class browser {
|
|
|
264
267
|
directory === '*' ? installed : directory,
|
|
265
268
|
))]
|
|
266
269
|
}
|
|
267
|
-
const
|
|
270
|
+
const external = options.cdp_ext && options.cdp_ext_install === false
|
|
271
|
+
const proc = child_process.spawn(options.executable_path || browser.find_executable_path(), [...util.args_to_strings(args), options.bootstrap_url ?? 'about:blank'], {
|
|
268
272
|
windowsHide: options.windowsHide ?? (args.headless != null && args.headless !== false),
|
|
269
|
-
|
|
273
|
+
detached: external,
|
|
274
|
+
stdio: external ? 'ignore' : ['ignore', 'ignore', 'pipe'],
|
|
270
275
|
})
|
|
276
|
+
if (external) {
|
|
277
|
+
await once(proc, 'spawn')
|
|
278
|
+
proc.unref()
|
|
279
|
+
return { proc }
|
|
280
|
+
}
|
|
271
281
|
let diagnostic = ''
|
|
272
282
|
const ready = util.on_first(proc, options.cdp_ext ? 'spawn exit' : 'ready exit', options.connect_timeout_ms ?? 15000)
|
|
273
283
|
const data = chunk => {
|
|
@@ -359,11 +369,23 @@ export class cdp extends EventTarget {
|
|
|
359
369
|
if (this.server) throw new Error('Reverse server is already listening')
|
|
360
370
|
const accept = (socket, origin) => {
|
|
361
371
|
socket.addEventListener('message', event => {
|
|
362
|
-
const
|
|
363
|
-
|
|
372
|
+
const message = JSON.parse(event.data)
|
|
373
|
+
const id = message.browser
|
|
374
|
+
const extension_id = message.extension_id ?? id
|
|
375
|
+
const record = this.browsers[id]
|
|
376
|
+
if (typeof id !== 'string' || extension_id !== origin.slice('chrome-extension://'.length) ||
|
|
377
|
+
(record?.reverse_token && message.token !== record.reverse_token) ||
|
|
378
|
+
(id !== extension_id && !record?.reverse_token) ||
|
|
364
379
|
this.reverse_sockets[id]?.readyState < WebSocket.CLOSING) {
|
|
365
|
-
return socket.close(1008, 'Invalid or already connected
|
|
380
|
+
return socket.close(1008, 'Invalid or already connected reverse browser')
|
|
366
381
|
}
|
|
382
|
+
if (message.error) {
|
|
383
|
+
const error = new Error(message.error)
|
|
384
|
+
if (record) record.reverse_error = error
|
|
385
|
+
util.emit(this, 'reverse_' + encodeURIComponent(id), error)
|
|
386
|
+
return socket.close(1008, 'Chrome profile already connected')
|
|
387
|
+
}
|
|
388
|
+
if (record) record.extension_id = extension_id
|
|
367
389
|
this.reverse_sockets[id] = socket
|
|
368
390
|
socket.addEventListener('close', () => {
|
|
369
391
|
if (this.reverse_sockets[id] === socket) delete this.reverse_sockets[id]
|
|
@@ -373,6 +395,9 @@ export class cdp extends EventTarget {
|
|
|
373
395
|
}, { once: true })
|
|
374
396
|
}
|
|
375
397
|
const fetch = (request, server) => {
|
|
398
|
+
if (request.method === 'GET' && new URL(request.url).pathname === '/cdp_ext') {
|
|
399
|
+
return new Response('CDP.js browser connection')
|
|
400
|
+
}
|
|
376
401
|
const origin = request.headers.get('origin')
|
|
377
402
|
if (request.headers.get('upgrade') !== 'websocket' ||
|
|
378
403
|
!origin?.startsWith('chrome-extension://')) {
|
|
@@ -594,6 +619,21 @@ export class cdp extends EventTarget {
|
|
|
594
619
|
if (record.reverse || record.cdp_ext) {
|
|
595
620
|
await this.server_closing
|
|
596
621
|
if (!this.server) this.listen_reverse()
|
|
622
|
+
let host = this.server.addr.hostname
|
|
623
|
+
if (host === '0.0.0.0') host = '127.0.0.1'
|
|
624
|
+
if (host === '::') host = '::1'
|
|
625
|
+
const address = (host.includes(':') ? '[' + host + ']' : host) + ':' + this.server.addr.port
|
|
626
|
+
record.reverse_url = 'ws://' + address
|
|
627
|
+
if (record.cdp_ext) {
|
|
628
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) throw new Error('URL bootstrap requires a loopback reverse listener')
|
|
629
|
+
record.reverse_token ??= crypto.randomUUID()
|
|
630
|
+
const bootstrap = new URL('http://' + address + '/cdp_ext')
|
|
631
|
+
bootstrap.searchParams.set('browser', record.name)
|
|
632
|
+
bootstrap.searchParams.set('websocket_url', record.reverse_url)
|
|
633
|
+
bootstrap.searchParams.set('token', record.reverse_token)
|
|
634
|
+
record.bootstrap_url = bootstrap.href
|
|
635
|
+
}
|
|
636
|
+
delete record.reverse_error
|
|
597
637
|
}
|
|
598
638
|
let url = record.websocket_url
|
|
599
639
|
if (!record.reverse && !record.cdp_ext && !url && (record.http_url || record.port)) {
|
|
@@ -604,10 +644,12 @@ export class cdp extends EventTarget {
|
|
|
604
644
|
url = (await response.json()).webSocketDebuggerUrl
|
|
605
645
|
}
|
|
606
646
|
if (record.cdp_ext || (!record.reverse && !url)) {
|
|
607
|
-
|
|
647
|
+
const args = browser.build_args(record)
|
|
648
|
+
if (record.cdp_ext) record.cdp_ext_install ??= !!args['user-data-dir']
|
|
649
|
+
if ((record.proc?.exitCode === null && record.proc.signalCode === null) ||
|
|
650
|
+
(record.opener?.exitCode === null && record.opener.signalCode === null)) url = record.socket?.url
|
|
608
651
|
else {
|
|
609
|
-
|
|
610
|
-
if (record.cdp_ext) {
|
|
652
|
+
if (record.cdp_ext && record.cdp_ext_install) {
|
|
611
653
|
record.extension_path = path.resolve(
|
|
612
654
|
args['user-data-dir'] || path.join(this.base_path, record.name),
|
|
613
655
|
args['profile-directory'] || 'Default',
|
|
@@ -616,12 +658,7 @@ export class cdp extends EventTarget {
|
|
|
616
658
|
await fs.mkdir(record.extension_path, { recursive: true })
|
|
617
659
|
record.extension_path = await fs.realpath(record.extension_path)
|
|
618
660
|
record.extension_id = browser.extension_id(record.extension_path)
|
|
619
|
-
|
|
620
|
-
let host = this.server.addr.hostname
|
|
621
|
-
if (host === '0.0.0.0') host = '127.0.0.1'
|
|
622
|
-
if (host === '::') host = '::1'
|
|
623
|
-
manifest.cdp.websocket_url = 'ws://' + (host.includes(':') ? '[' + host + ']' : host) + ':' + this.server.addr.port
|
|
624
|
-
await fs.writeFile(path.join(record.extension_path, 'manifest.json'), JSON.stringify(manifest))
|
|
661
|
+
await fs.copyFile(new URL('./cdp_ext/manifest.json', import.meta.url), path.join(record.extension_path, 'manifest.json'))
|
|
625
662
|
await fs.copyFile(new URL('./cdp_ext/cdp_ext.js', import.meta.url), path.join(record.extension_path, 'cdp_ext.js'))
|
|
626
663
|
}
|
|
627
664
|
if (args['user-data-dir'] && record.update_preferences !== false) {
|
|
@@ -635,17 +672,28 @@ export class cdp extends EventTarget {
|
|
|
635
672
|
await browser.update_profile_preferences(path.join(args['user-data-dir'], 'Local State'), record.local_state)
|
|
636
673
|
}
|
|
637
674
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
675
|
+
if (!record.cdp_ext || record.cdp_ext_install) {
|
|
676
|
+
const launched = await browser.launch(record)
|
|
677
|
+
record.proc = launched.proc
|
|
678
|
+
if (record.cdp_ext) record.proc.once('exit', () => this._close_reverse_if_idle())
|
|
679
|
+
url = launched.url
|
|
680
|
+
}
|
|
642
681
|
}
|
|
643
682
|
}
|
|
644
683
|
if (record.reverse || record.cdp_ext) {
|
|
645
|
-
const id = record.extension_id ?? record.name
|
|
684
|
+
const id = record.cdp_ext ? record.name : record.extension_id ?? record.name
|
|
646
685
|
url = this.reverse_sockets[id]
|
|
647
686
|
if (url?.readyState !== WebSocket.OPEN) {
|
|
648
|
-
|
|
687
|
+
if (record.cdp_ext && !record.cdp_ext_install &&
|
|
688
|
+
!(record.opener?.exitCode === null && record.opener.signalCode === null)) {
|
|
689
|
+
record.opener = (await browser.launch(record)).proc
|
|
690
|
+
}
|
|
691
|
+
if (record.reverse_error) throw record.reverse_error
|
|
692
|
+
url = this.reverse_sockets[id]
|
|
693
|
+
if (url?.readyState !== WebSocket.OPEN) {
|
|
694
|
+
url = (await util.on_first(this, 'reverse_' + encodeURIComponent(id), record.connect_timeout_ms ?? 15000)).args[0].detail
|
|
695
|
+
}
|
|
696
|
+
if (url instanceof Error) throw url
|
|
649
697
|
}
|
|
650
698
|
}
|
|
651
699
|
const socket = record.socket = new jsrpc(url, record)
|
|
@@ -829,7 +877,7 @@ export class cdp extends EventTarget {
|
|
|
829
877
|
const { browser = 'main', target, ...native } = params
|
|
830
878
|
const spec = typeof browser === 'string' ? { name: browser } : browser
|
|
831
879
|
const record = this.browsers[spec.name] ??= {
|
|
832
|
-
user_data_dir: path.join(this.base_path, spec.name),
|
|
880
|
+
user_data_dir: ((spec.reverse ?? this.options.reverse) || (spec.cdp_ext ?? this.options.cdp_ext)) ? null : path.join(this.base_path, spec.name),
|
|
833
881
|
...this.options,
|
|
834
882
|
...spec,
|
|
835
883
|
args: {
|
package/cdp_ext/cdp_ext.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
const config =
|
|
1
|
+
const config = {
|
|
2
|
+
reconnect_ms: chrome.runtime.getManifest().cdp?.reconnect_ms ?? 5000,
|
|
3
|
+
}
|
|
4
|
+
const configured = chrome.storage.session.get('cdp_bootstrap').then(value => Object.assign(config, value.cdp_bootstrap))
|
|
2
5
|
const sessions = {}
|
|
3
6
|
const attached = {}
|
|
4
7
|
const attaching = {}
|
|
@@ -6,6 +9,58 @@ let socket
|
|
|
6
9
|
let disconnecting
|
|
7
10
|
let discovering = false
|
|
8
11
|
let auto_attach = {}
|
|
12
|
+
let bootstrapping
|
|
13
|
+
let checking = false
|
|
14
|
+
let connection_message
|
|
15
|
+
|
|
16
|
+
function report_connection(message) {
|
|
17
|
+
if (connection_message === message) return
|
|
18
|
+
connection_message = message
|
|
19
|
+
console.info('[cdp_ext] ' + message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function bootstrap(tab) {
|
|
23
|
+
const text = tab.pendingUrl ?? tab.url ?? ''
|
|
24
|
+
if (!/^http:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?\/cdp_ext\?/.test(text)) return
|
|
25
|
+
const url = new URL(text)
|
|
26
|
+
const settings = {
|
|
27
|
+
browser: url.searchParams.get('browser'),
|
|
28
|
+
websocket_url: url.searchParams.get('websocket_url'),
|
|
29
|
+
token: url.searchParams.get('token'),
|
|
30
|
+
}
|
|
31
|
+
if (!settings.browser || !settings.websocket_url || !settings.token) return
|
|
32
|
+
const endpoint = new URL(settings.websocket_url)
|
|
33
|
+
if (endpoint.protocol !== 'ws:' || endpoint.host !== url.host) return
|
|
34
|
+
await configured
|
|
35
|
+
while (bootstrapping) await bootstrapping
|
|
36
|
+
return bootstrapping = (async () => {
|
|
37
|
+
const changed = ['browser', 'websocket_url', 'token'].some(key => settings[key] !== config[key])
|
|
38
|
+
if (changed && config.browser && socket?.readyState < WebSocket.CLOSING) {
|
|
39
|
+
const probe = new WebSocket(settings.websocket_url)
|
|
40
|
+
probe.onopen = () => probe.send(JSON.stringify({
|
|
41
|
+
browser: settings.browser,
|
|
42
|
+
extension_id: chrome.runtime.id,
|
|
43
|
+
token: settings.token,
|
|
44
|
+
error: 'This Chrome profile is already connected as ' + config.browser,
|
|
45
|
+
}))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (changed) {
|
|
49
|
+
Object.assign(config, settings)
|
|
50
|
+
await chrome.storage.session.set({ cdp_bootstrap: settings })
|
|
51
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
52
|
+
await new Promise(resolve => {
|
|
53
|
+
socket.addEventListener('close', resolve, { once: true })
|
|
54
|
+
socket.close()
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
await disconnecting
|
|
58
|
+
}
|
|
59
|
+
await connect()
|
|
60
|
+
})().finally(() => {
|
|
61
|
+
bootstrapping = null
|
|
62
|
+
})
|
|
63
|
+
}
|
|
9
64
|
|
|
10
65
|
function send(message) {
|
|
11
66
|
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message))
|
|
@@ -160,12 +215,14 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
160
215
|
})
|
|
161
216
|
chrome.debugger.onDetach.addListener(detached)
|
|
162
217
|
chrome.tabs.onCreated.addListener(tab => {
|
|
218
|
+
bootstrap(tab)
|
|
163
219
|
if (discovering) send({
|
|
164
220
|
method: 'Target.targetCreated',
|
|
165
221
|
params: { targetInfo: target_info(tab) },
|
|
166
222
|
})
|
|
167
223
|
})
|
|
168
224
|
chrome.tabs.onUpdated.addListener((id, changes, tab) => {
|
|
225
|
+
bootstrap(tab)
|
|
169
226
|
if (discovering) send({
|
|
170
227
|
method: 'Target.targetInfoChanged',
|
|
171
228
|
params: { targetInfo: target_info(tab) },
|
|
@@ -196,12 +253,45 @@ for (const [name, options] of Object.entries({
|
|
|
196
253
|
}), { urls: ['<all_urls>'] }, options)
|
|
197
254
|
}
|
|
198
255
|
|
|
199
|
-
function connect() {
|
|
200
|
-
|
|
256
|
+
async function connect() {
|
|
257
|
+
await configured
|
|
258
|
+
if (!config.browser || !config.websocket_url || !config.token) return
|
|
259
|
+
if (disconnecting || checking) return
|
|
201
260
|
if (socket?.readyState === WebSocket.CONNECTING) return
|
|
202
261
|
if (socket?.readyState === WebSocket.OPEN) return socket.send('')
|
|
203
|
-
const
|
|
204
|
-
|
|
262
|
+
const endpoint = config.websocket_url
|
|
263
|
+
const url = new URL(endpoint)
|
|
264
|
+
url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:'
|
|
265
|
+
url.pathname = '/cdp_ext'
|
|
266
|
+
url.search = ''
|
|
267
|
+
url.hash = ''
|
|
268
|
+
let available = false
|
|
269
|
+
checking = true
|
|
270
|
+
try {
|
|
271
|
+
const response = await fetch(url, {
|
|
272
|
+
cache: 'no-store',
|
|
273
|
+
signal: AbortSignal.timeout(2000),
|
|
274
|
+
})
|
|
275
|
+
available = response.ok && await response.text() === 'CDP.js browser connection'
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (!(error instanceof TypeError) && error.name !== 'TimeoutError') throw error
|
|
278
|
+
} finally {
|
|
279
|
+
checking = false
|
|
280
|
+
}
|
|
281
|
+
if (endpoint !== config.websocket_url) return connect()
|
|
282
|
+
if (!available) {
|
|
283
|
+
report_connection('Waiting for CDP.js server at ' + endpoint + '; retrying automatically.')
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
const connection = socket = new WebSocket(endpoint)
|
|
287
|
+
connection.onopen = () => {
|
|
288
|
+
report_connection('Connected to CDP.js server at ' + endpoint + '.')
|
|
289
|
+
connection.send(JSON.stringify({
|
|
290
|
+
browser: config.browser,
|
|
291
|
+
extension_id: chrome.runtime.id,
|
|
292
|
+
token: config.token,
|
|
293
|
+
}))
|
|
294
|
+
}
|
|
205
295
|
connection.onmessage = async event => {
|
|
206
296
|
if (!event.data) return
|
|
207
297
|
const request = JSON.parse(event.data)
|
|
@@ -248,6 +338,7 @@ function connect() {
|
|
|
248
338
|
if (id != null && connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify(response))
|
|
249
339
|
}
|
|
250
340
|
connection.onclose = () => {
|
|
341
|
+
report_connection('Waiting for CDP.js server at ' + endpoint + '; retrying automatically.')
|
|
251
342
|
discovering = false
|
|
252
343
|
disconnecting = Promise.all(Object.keys(attached).map(id => {
|
|
253
344
|
const source = { tabId: Number(id) }
|
|
@@ -257,8 +348,19 @@ function connect() {
|
|
|
257
348
|
}
|
|
258
349
|
}
|
|
259
350
|
|
|
260
|
-
|
|
351
|
+
async function start() {
|
|
352
|
+
await configured
|
|
353
|
+
if (!config.browser) {
|
|
354
|
+
for (const tab of (await chrome.tabs.query({})).reverse()) {
|
|
355
|
+
if (config.browser) break
|
|
356
|
+
await bootstrap(tab)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
await connect()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
chrome.runtime.onStartup.addListener(start)
|
|
261
363
|
chrome.alarms.onAlarm.addListener(connect)
|
|
262
364
|
chrome.alarms.create('cdp-reconnect', { periodInMinutes: 0.5 })
|
|
263
|
-
setInterval(connect, config.reconnect_ms
|
|
264
|
-
|
|
365
|
+
setInterval(connect, config.reconnect_ms)
|
|
366
|
+
start()
|
package/cdp_ext/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "CDP reverse connection",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"minimum_chrome_version": "125",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"accessibilityFeatures.read",
|
|
@@ -68,9 +68,5 @@
|
|
|
68
68
|
"incognito": "spanning",
|
|
69
69
|
"background": {
|
|
70
70
|
"service_worker": "cdp_ext.js"
|
|
71
|
-
},
|
|
72
|
-
"cdp": {
|
|
73
|
-
"websocket_url": "ws://127.0.0.1:9223",
|
|
74
|
-
"reconnect_ms": 5000
|
|
75
71
|
}
|
|
76
72
|
}
|
package/package.json
CHANGED