parsek-cdp 0.1.0__py3-none-any.whl
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.
- parsek_cdp/__init__.py +55 -0
- parsek_cdp/_logging.py +49 -0
- parsek_cdp/cdp/__init__.py +247 -0
- parsek_cdp/cdp/accessibility/__init__.py +15 -0
- parsek_cdp/cdp/accessibility/events.py +35 -0
- parsek_cdp/cdp/accessibility/functions.py +202 -0
- parsek_cdp/cdp/accessibility/types.py +235 -0
- parsek_cdp/cdp/animation/__init__.py +15 -0
- parsek_cdp/cdp/animation/events.py +52 -0
- parsek_cdp/cdp/animation/functions.py +140 -0
- parsek_cdp/cdp/animation/types.py +113 -0
- parsek_cdp/cdp/audits/__init__.py +15 -0
- parsek_cdp/cdp/audits/events.py +21 -0
- parsek_cdp/cdp/audits/functions.py +95 -0
- parsek_cdp/cdp/audits/types.py +1051 -0
- parsek_cdp/cdp/autofill/__init__.py +15 -0
- parsek_cdp/cdp/autofill/events.py +27 -0
- parsek_cdp/cdp/autofill/functions.py +64 -0
- parsek_cdp/cdp/autofill/types.py +108 -0
- parsek_cdp/cdp/backgroundservice/__init__.py +15 -0
- parsek_cdp/cdp/backgroundservice/events.py +40 -0
- parsek_cdp/cdp/backgroundservice/functions.py +59 -0
- parsek_cdp/cdp/backgroundservice/types.py +63 -0
- parsek_cdp/cdp/bluetoothemulation/__init__.py +15 -0
- parsek_cdp/cdp/bluetoothemulation/events.py +70 -0
- parsek_cdp/cdp/bluetoothemulation/functions.py +247 -0
- parsek_cdp/cdp/bluetoothemulation/types.py +120 -0
- parsek_cdp/cdp/browser/__init__.py +15 -0
- parsek_cdp/cdp/browser/events.py +46 -0
- parsek_cdp/cdp/browser/functions.py +350 -0
- parsek_cdp/cdp/browser/types.py +158 -0
- parsek_cdp/cdp/cachestorage/__init__.py +15 -0
- parsek_cdp/cdp/cachestorage/events.py +13 -0
- parsek_cdp/cdp/cachestorage/functions.py +126 -0
- parsek_cdp/cdp/cachestorage/types.py +89 -0
- parsek_cdp/cdp/cast/__init__.py +15 -0
- parsek_cdp/cdp/cast/events.py +38 -0
- parsek_cdp/cdp/cast/functions.py +77 -0
- parsek_cdp/cdp/cast/types.py +23 -0
- parsek_cdp/cdp/console/__init__.py +15 -0
- parsek_cdp/cdp/console/events.py +22 -0
- parsek_cdp/cdp/console/functions.py +36 -0
- parsek_cdp/cdp/console/types.py +30 -0
- parsek_cdp/cdp/css/__init__.py +15 -0
- parsek_cdp/cdp/css/events.py +79 -0
- parsek_cdp/cdp/css/functions.py +788 -0
- parsek_cdp/cdp/css/types.py +742 -0
- parsek_cdp/cdp/debugger/__init__.py +15 -0
- parsek_cdp/cdp/debugger/events.py +164 -0
- parsek_cdp/cdp/debugger/functions.py +663 -0
- parsek_cdp/cdp/debugger/types.py +171 -0
- parsek_cdp/cdp/deviceaccess/__init__.py +15 -0
- parsek_cdp/cdp/deviceaccess/events.py +30 -0
- parsek_cdp/cdp/deviceaccess/functions.py +54 -0
- parsek_cdp/cdp/deviceaccess/types.py +28 -0
- parsek_cdp/cdp/deviceorientation/__init__.py +15 -0
- parsek_cdp/cdp/deviceorientation/events.py +13 -0
- parsek_cdp/cdp/deviceorientation/functions.py +35 -0
- parsek_cdp/cdp/deviceorientation/types.py +10 -0
- parsek_cdp/cdp/dom/__init__.py +15 -0
- parsek_cdp/cdp/dom/events.py +228 -0
- parsek_cdp/cdp/dom/functions.py +1033 -0
- parsek_cdp/cdp/dom/types.py +293 -0
- parsek_cdp/cdp/domdebugger/__init__.py +15 -0
- parsek_cdp/cdp/domdebugger/events.py +13 -0
- parsek_cdp/cdp/domdebugger/functions.py +155 -0
- parsek_cdp/cdp/domdebugger/types.py +57 -0
- parsek_cdp/cdp/domsnapshot/__init__.py +15 -0
- parsek_cdp/cdp/domsnapshot/events.py +13 -0
- parsek_cdp/cdp/domsnapshot/functions.py +111 -0
- parsek_cdp/cdp/domsnapshot/types.py +312 -0
- parsek_cdp/cdp/domstorage/__init__.py +15 -0
- parsek_cdp/cdp/domstorage/events.py +60 -0
- parsek_cdp/cdp/domstorage/functions.py +79 -0
- parsek_cdp/cdp/domstorage/types.py +30 -0
- parsek_cdp/cdp/emulation/__init__.py +15 -0
- parsek_cdp/cdp/emulation/events.py +17 -0
- parsek_cdp/cdp/emulation/functions.py +688 -0
- parsek_cdp/cdp/emulation/types.py +305 -0
- parsek_cdp/cdp/eventbreakpoints/__init__.py +15 -0
- parsek_cdp/cdp/eventbreakpoints/events.py +13 -0
- parsek_cdp/cdp/eventbreakpoints/functions.py +41 -0
- parsek_cdp/cdp/eventbreakpoints/types.py +10 -0
- parsek_cdp/cdp/extensions/__init__.py +15 -0
- parsek_cdp/cdp/extensions/events.py +13 -0
- parsek_cdp/cdp/extensions/functions.py +118 -0
- parsek_cdp/cdp/extensions/types.py +19 -0
- parsek_cdp/cdp/fedcm/__init__.py +15 -0
- parsek_cdp/cdp/fedcm/events.py +45 -0
- parsek_cdp/cdp/fedcm/functions.py +93 -0
- parsek_cdp/cdp/fedcm/types.py +72 -0
- parsek_cdp/cdp/fetch/__init__.py +15 -0
- parsek_cdp/cdp/fetch/events.py +82 -0
- parsek_cdp/cdp/fetch/functions.py +215 -0
- parsek_cdp/cdp/fetch/types.py +81 -0
- parsek_cdp/cdp/filesystem/__init__.py +15 -0
- parsek_cdp/cdp/filesystem/events.py +13 -0
- parsek_cdp/cdp/filesystem/functions.py +36 -0
- parsek_cdp/cdp/filesystem/types.py +54 -0
- parsek_cdp/cdp/headlessexperimental/__init__.py +15 -0
- parsek_cdp/cdp/headlessexperimental/events.py +13 -0
- parsek_cdp/cdp/headlessexperimental/functions.py +77 -0
- parsek_cdp/cdp/headlessexperimental/types.py +24 -0
- parsek_cdp/cdp/heapprofiler/__init__.py +15 -0
- parsek_cdp/cdp/heapprofiler/events.py +65 -0
- parsek_cdp/cdp/heapprofiler/functions.py +193 -0
- parsek_cdp/cdp/heapprofiler/types.py +57 -0
- parsek_cdp/cdp/indexeddb/__init__.py +15 -0
- parsek_cdp/cdp/indexeddb/events.py +13 -0
- parsek_cdp/cdp/indexeddb/functions.py +232 -0
- parsek_cdp/cdp/indexeddb/types.py +120 -0
- parsek_cdp/cdp/input/__init__.py +15 -0
- parsek_cdp/cdp/input/events.py +25 -0
- parsek_cdp/cdp/input/functions.py +361 -0
- parsek_cdp/cdp/input/types.py +87 -0
- parsek_cdp/cdp/inspector/__init__.py +15 -0
- parsek_cdp/cdp/inspector/events.py +41 -0
- parsek_cdp/cdp/inspector/functions.py +27 -0
- parsek_cdp/cdp/inspector/types.py +10 -0
- parsek_cdp/cdp/io/__init__.py +15 -0
- parsek_cdp/cdp/io/events.py +13 -0
- parsek_cdp/cdp/io/functions.py +77 -0
- parsek_cdp/cdp/io/types.py +13 -0
- parsek_cdp/cdp/layertree/__init__.py +15 -0
- parsek_cdp/cdp/layertree/events.py +36 -0
- parsek_cdp/cdp/layertree/functions.py +178 -0
- parsek_cdp/cdp/layertree/types.py +104 -0
- parsek_cdp/cdp/log/__init__.py +15 -0
- parsek_cdp/cdp/log/events.py +22 -0
- parsek_cdp/cdp/log/functions.py +54 -0
- parsek_cdp/cdp/log/types.py +57 -0
- parsek_cdp/cdp/media/__init__.py +15 -0
- parsek_cdp/cdp/media/events.py +87 -0
- parsek_cdp/cdp/media/functions.py +27 -0
- parsek_cdp/cdp/media/types.py +101 -0
- parsek_cdp/cdp/memory/__init__.py +15 -0
- parsek_cdp/cdp/memory/events.py +13 -0
- parsek_cdp/cdp/memory/functions.py +164 -0
- parsek_cdp/cdp/memory/types.py +71 -0
- parsek_cdp/cdp/mixins/__init__.py +21 -0
- parsek_cdp/cdp/mixins/datatype.py +123 -0
- parsek_cdp/cdp/mixins/event.py +78 -0
- parsek_cdp/cdp/mixins/namespace.py +31 -0
- parsek_cdp/cdp/network/__init__.py +15 -0
- parsek_cdp/cdp/network/events.py +726 -0
- parsek_cdp/cdp/network/functions.py +734 -0
- parsek_cdp/cdp/network/types.py +1247 -0
- parsek_cdp/cdp/overlay/__init__.py +15 -0
- parsek_cdp/cdp/overlay/events.py +54 -0
- parsek_cdp/cdp/overlay/functions.py +407 -0
- parsek_cdp/cdp/overlay/types.py +328 -0
- parsek_cdp/cdp/page/__init__.py +15 -0
- parsek_cdp/cdp/page/events.py +416 -0
- parsek_cdp/cdp/page/functions.py +1081 -0
- parsek_cdp/cdp/page/types.py +1088 -0
- parsek_cdp/cdp/performance/__init__.py +15 -0
- parsek_cdp/cdp/performance/events.py +24 -0
- parsek_cdp/cdp/performance/functions.py +63 -0
- parsek_cdp/cdp/performance/types.py +22 -0
- parsek_cdp/cdp/performancetimeline/__init__.py +15 -0
- parsek_cdp/cdp/performancetimeline/events.py +22 -0
- parsek_cdp/cdp/performancetimeline/functions.py +30 -0
- parsek_cdp/cdp/performancetimeline/types.py +85 -0
- parsek_cdp/cdp/preload/__init__.py +15 -0
- parsek_cdp/cdp/preload/events.py +116 -0
- parsek_cdp/cdp/preload/functions.py +25 -0
- parsek_cdp/cdp/preload/types.py +267 -0
- parsek_cdp/cdp/profiler/__init__.py +15 -0
- parsek_cdp/cdp/profiler/events.py +64 -0
- parsek_cdp/cdp/profiler/functions.py +134 -0
- parsek_cdp/cdp/profiler/types.py +105 -0
- parsek_cdp/cdp/pwa/__init__.py +15 -0
- parsek_cdp/cdp/pwa/events.py +13 -0
- parsek_cdp/cdp/pwa/functions.py +196 -0
- parsek_cdp/cdp/pwa/types.py +45 -0
- parsek_cdp/cdp/runtime/__init__.py +15 -0
- parsek_cdp/cdp/runtime/events.py +123 -0
- parsek_cdp/cdp/runtime/functions.py +552 -0
- parsek_cdp/cdp/runtime/types.py +319 -0
- parsek_cdp/cdp/schema/__init__.py +15 -0
- parsek_cdp/cdp/schema/events.py +13 -0
- parsek_cdp/cdp/schema/functions.py +32 -0
- parsek_cdp/cdp/schema/types.py +22 -0
- parsek_cdp/cdp/security/__init__.py +15 -0
- parsek_cdp/cdp/security/events.py +64 -0
- parsek_cdp/cdp/security/functions.py +66 -0
- parsek_cdp/cdp/security/types.py +168 -0
- parsek_cdp/cdp/serviceworker/__init__.py +15 -0
- parsek_cdp/cdp/serviceworker/events.py +43 -0
- parsek_cdp/cdp/serviceworker/functions.py +115 -0
- parsek_cdp/cdp/serviceworker/types.py +95 -0
- parsek_cdp/cdp/storage/__init__.py +15 -0
- parsek_cdp/cdp/storage/events.py +288 -0
- parsek_cdp/cdp/storage/functions.py +571 -0
- parsek_cdp/cdp/storage/types.py +656 -0
- parsek_cdp/cdp/systeminfo/__init__.py +15 -0
- parsek_cdp/cdp/systeminfo/events.py +13 -0
- parsek_cdp/cdp/systeminfo/functions.py +75 -0
- parsek_cdp/cdp/systeminfo/types.py +132 -0
- parsek_cdp/cdp/target/__init__.py +15 -0
- parsek_cdp/cdp/target/events.py +109 -0
- parsek_cdp/cdp/target/functions.py +424 -0
- parsek_cdp/cdp/target/types.py +83 -0
- parsek_cdp/cdp/tethering/__init__.py +15 -0
- parsek_cdp/cdp/tethering/events.py +22 -0
- parsek_cdp/cdp/tethering/functions.py +35 -0
- parsek_cdp/cdp/tethering/types.py +10 -0
- parsek_cdp/cdp/tracing/__init__.py +15 -0
- parsek_cdp/cdp/tracing/events.py +61 -0
- parsek_cdp/cdp/tracing/functions.py +119 -0
- parsek_cdp/cdp/tracing/types.py +81 -0
- parsek_cdp/cdp/webaudio/__init__.py +15 -0
- parsek_cdp/cdp/webaudio/events.py +184 -0
- parsek_cdp/cdp/webaudio/functions.py +51 -0
- parsek_cdp/cdp/webaudio/types.py +155 -0
- parsek_cdp/cdp/webauthn/__init__.py +15 -0
- parsek_cdp/cdp/webauthn/events.py +69 -0
- parsek_cdp/cdp/webauthn/functions.py +214 -0
- parsek_cdp/cdp/webauthn/types.py +96 -0
- parsek_cdp/core/__init__.py +18 -0
- parsek_cdp/core/browser.py +240 -0
- parsek_cdp/core/browser_context.py +58 -0
- parsek_cdp/core/element.py +405 -0
- parsek_cdp/core/feature.py +376 -0
- parsek_cdp/core/frame.py +351 -0
- parsek_cdp/core/pagable.py +175 -0
- parsek_cdp/core/page.py +163 -0
- parsek_cdp/core/target.py +490 -0
- parsek_cdp/features/__init__.py +14 -0
- parsek_cdp/features/request_listener/__init__.py +256 -0
- parsek_cdp/features/request_listener/events.py +48 -0
- parsek_cdp/features/request_listener/types.py +235 -0
- parsek_cdp/generate.py +642 -0
- parsek_cdp/parsek/__init__.py +18 -0
- parsek_cdp/parsek/events.py +38 -0
- parsek_cdp/parsek/types.py +36 -0
- parsek_cdp/py.typed +0 -0
- parsek_cdp-0.1.0.dist-info/METADATA +277 -0
- parsek_cdp-0.1.0.dist-info/RECORD +242 -0
- parsek_cdp-0.1.0.dist-info/WHEEL +5 -0
- parsek_cdp-0.1.0.dist-info/licenses/LICENSE +201 -0
- parsek_cdp-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The WebAuthn CDP domain (generated).
|
|
2
|
+
|
|
3
|
+
Importing ``WebAuthn`` from ``parsek_cdp.cdp`` gives a namespace with this
|
|
4
|
+
domain's events and types as attributes (``WebAuthn.SomeEvent`` /
|
|
5
|
+
``WebAuthn.SomeType``); commands run on a target via ``page.cdp.WebAuthn``.
|
|
6
|
+
"""
|
|
7
|
+
from . import events, functions, types
|
|
8
|
+
from .functions import WebAuthn
|
|
9
|
+
from .events import * # noqa: F401,F403 -- expose events on the namespace
|
|
10
|
+
from .types import * # noqa: F401,F403 -- expose types on the namespace
|
|
11
|
+
|
|
12
|
+
#: Protocol domain name (used by features that declare ``domains=(WebAuthn,)``).
|
|
13
|
+
DOMAIN = "WebAuthn"
|
|
14
|
+
|
|
15
|
+
__all__ = ["events", "functions", "types", "WebAuthn", "DOMAIN"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Events for the WebAuthn domain (generated)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from ..mixins.datatype import FieldMeta
|
|
8
|
+
from ..mixins.event import Event, register_event
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .types import (
|
|
12
|
+
AuthenticatorId,
|
|
13
|
+
Credential,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@register_event("WebAuthn.credentialAdded")
|
|
17
|
+
@dataclass
|
|
18
|
+
class CredentialAdded(Event):
|
|
19
|
+
"""Triggered when a credential is added to an authenticator."""
|
|
20
|
+
authenticator_id: AuthenticatorId
|
|
21
|
+
credential: Credential
|
|
22
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
23
|
+
FieldMeta('authenticator_id', 'authenticatorId', False, 'primitive'),
|
|
24
|
+
FieldMeta('credential', 'credential', False, 'object', ref='WebAuthn.Credential'),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register_event("WebAuthn.credentialDeleted")
|
|
29
|
+
@dataclass
|
|
30
|
+
class CredentialDeleted(Event):
|
|
31
|
+
"""
|
|
32
|
+
Triggered when a credential is deleted, e.g. through
|
|
33
|
+
PublicKeyCredential.signalUnknownCredential().
|
|
34
|
+
"""
|
|
35
|
+
authenticator_id: AuthenticatorId
|
|
36
|
+
credential_id: str
|
|
37
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
38
|
+
FieldMeta('authenticator_id', 'authenticatorId', False, 'primitive'),
|
|
39
|
+
FieldMeta('credential_id', 'credentialId', False, 'primitive'),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@register_event("WebAuthn.credentialUpdated")
|
|
44
|
+
@dataclass
|
|
45
|
+
class CredentialUpdated(Event):
|
|
46
|
+
"""
|
|
47
|
+
Triggered when a credential is updated, e.g. through
|
|
48
|
+
PublicKeyCredential.signalCurrentUserDetails().
|
|
49
|
+
"""
|
|
50
|
+
authenticator_id: AuthenticatorId
|
|
51
|
+
credential: Credential
|
|
52
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
53
|
+
FieldMeta('authenticator_id', 'authenticatorId', False, 'primitive'),
|
|
54
|
+
FieldMeta('credential', 'credential', False, 'object', ref='WebAuthn.Credential'),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register_event("WebAuthn.credentialAsserted")
|
|
59
|
+
@dataclass
|
|
60
|
+
class CredentialAsserted(Event):
|
|
61
|
+
"""Triggered when a credential is used in a webauthn assertion."""
|
|
62
|
+
authenticator_id: AuthenticatorId
|
|
63
|
+
credential: Credential
|
|
64
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
65
|
+
FieldMeta('authenticator_id', 'authenticatorId', False, 'primitive'),
|
|
66
|
+
FieldMeta('credential', 'credential', False, 'object', ref='WebAuthn.Credential'),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = ["CredentialAdded", "CredentialAsserted", "CredentialDeleted", "CredentialUpdated"]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Commands for the WebAuthn domain (generated)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from ..mixins.datatype import DataType, FieldMeta, encode
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .types import (
|
|
11
|
+
AuthenticatorId,
|
|
12
|
+
Credential,
|
|
13
|
+
VirtualAuthenticatorOptions,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AddVirtualAuthenticatorReturn(DataType):
|
|
18
|
+
"""Return value of :meth:`WebAuthn.add_virtual_authenticator`."""
|
|
19
|
+
authenticator_id: AuthenticatorId
|
|
20
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
21
|
+
FieldMeta('authenticator_id', 'authenticatorId', False, 'primitive'),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class GetCredentialReturn(DataType):
|
|
27
|
+
"""Return value of :meth:`WebAuthn.get_credential`."""
|
|
28
|
+
credential: Credential
|
|
29
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
30
|
+
FieldMeta('credential', 'credential', False, 'object', ref='WebAuthn.Credential'),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GetCredentialsReturn(DataType):
|
|
36
|
+
"""Return value of :meth:`WebAuthn.get_credentials`."""
|
|
37
|
+
credentials: List[Credential]
|
|
38
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
39
|
+
FieldMeta('credentials', 'credentials', False, 'array', inner=FieldMeta('', '', False, 'object', ref='WebAuthn.Credential')),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WebAuthn:
|
|
44
|
+
"""Commands of the WebAuthn domain, bound to a target."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, target: Any) -> None:
|
|
47
|
+
self._target = target
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def enable(self, *, enable_ui: Optional[bool] = None) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Enable the WebAuthn domain and start intercepting credential storage and
|
|
53
|
+
retrieval with a virtual authenticator.
|
|
54
|
+
:param enable_ui: Whether to enable the WebAuthn user interface. Enabling the UI is
|
|
55
|
+
recommended for debugging and demo purposes, as it is closer to the real
|
|
56
|
+
experience. Disabling the UI is recommended for automated testing.
|
|
57
|
+
Supported at the embedder's discretion if UI is available.
|
|
58
|
+
Defaults to false.
|
|
59
|
+
"""
|
|
60
|
+
_params: Dict[str, Any] = {}
|
|
61
|
+
if enable_ui is not None:
|
|
62
|
+
_params['enableUI'] = encode(FieldMeta('', '', False, 'primitive'), enable_ui)
|
|
63
|
+
_result = await self._target.send('WebAuthn.enable', _params)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
async def disable(self) -> None:
|
|
67
|
+
"""Disable the WebAuthn domain."""
|
|
68
|
+
_params: Dict[str, Any] = {}
|
|
69
|
+
_result = await self._target.send('WebAuthn.disable', _params)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
async def add_virtual_authenticator(self, *, options: VirtualAuthenticatorOptions) -> AddVirtualAuthenticatorReturn:
|
|
73
|
+
"""
|
|
74
|
+
Creates and adds a virtual authenticator.
|
|
75
|
+
:param options:
|
|
76
|
+
"""
|
|
77
|
+
_params: Dict[str, Any] = {}
|
|
78
|
+
_params['options'] = encode(FieldMeta('', '', False, 'object', ref='WebAuthn.VirtualAuthenticatorOptions'), options)
|
|
79
|
+
_result = await self._target.send('WebAuthn.addVirtualAuthenticator', _params)
|
|
80
|
+
return AddVirtualAuthenticatorReturn.from_json(_result)
|
|
81
|
+
|
|
82
|
+
async def set_response_override_bits(self, *, authenticator_id: AuthenticatorId, is_bogus_signature: Optional[bool] = None, is_bad_uv: Optional[bool] = None, is_bad_up: Optional[bool] = None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Resets parameters isBogusSignature, isBadUV, isBadUP to false if they are not present.
|
|
85
|
+
:param authenticator_id:
|
|
86
|
+
:param is_bogus_signature: If isBogusSignature is set, overrides the signature in the authenticator response to be zero.
|
|
87
|
+
Defaults to false.
|
|
88
|
+
:param is_bad_uv: If isBadUV is set, overrides the UV bit in the flags in the authenticator response to
|
|
89
|
+
be zero. Defaults to false.
|
|
90
|
+
:param is_bad_up: If isBadUP is set, overrides the UP bit in the flags in the authenticator response to
|
|
91
|
+
be zero. Defaults to false.
|
|
92
|
+
"""
|
|
93
|
+
_params: Dict[str, Any] = {}
|
|
94
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
95
|
+
if is_bogus_signature is not None:
|
|
96
|
+
_params['isBogusSignature'] = encode(FieldMeta('', '', False, 'primitive'), is_bogus_signature)
|
|
97
|
+
if is_bad_uv is not None:
|
|
98
|
+
_params['isBadUV'] = encode(FieldMeta('', '', False, 'primitive'), is_bad_uv)
|
|
99
|
+
if is_bad_up is not None:
|
|
100
|
+
_params['isBadUP'] = encode(FieldMeta('', '', False, 'primitive'), is_bad_up)
|
|
101
|
+
_result = await self._target.send('WebAuthn.setResponseOverrideBits', _params)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
async def remove_virtual_authenticator(self, *, authenticator_id: AuthenticatorId) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Removes the given authenticator.
|
|
107
|
+
:param authenticator_id:
|
|
108
|
+
"""
|
|
109
|
+
_params: Dict[str, Any] = {}
|
|
110
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
111
|
+
_result = await self._target.send('WebAuthn.removeVirtualAuthenticator', _params)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
async def add_credential(self, *, authenticator_id: AuthenticatorId, credential: Credential) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Adds the credential to the specified authenticator.
|
|
117
|
+
:param authenticator_id:
|
|
118
|
+
:param credential:
|
|
119
|
+
"""
|
|
120
|
+
_params: Dict[str, Any] = {}
|
|
121
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
122
|
+
_params['credential'] = encode(FieldMeta('', '', False, 'object', ref='WebAuthn.Credential'), credential)
|
|
123
|
+
_result = await self._target.send('WebAuthn.addCredential', _params)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
async def get_credential(self, *, authenticator_id: AuthenticatorId, credential_id: str) -> GetCredentialReturn:
|
|
127
|
+
"""
|
|
128
|
+
Returns a single credential stored in the given virtual authenticator that
|
|
129
|
+
matches the credential ID.
|
|
130
|
+
:param authenticator_id:
|
|
131
|
+
:param credential_id:
|
|
132
|
+
"""
|
|
133
|
+
_params: Dict[str, Any] = {}
|
|
134
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
135
|
+
_params['credentialId'] = encode(FieldMeta('', '', False, 'primitive'), credential_id)
|
|
136
|
+
_result = await self._target.send('WebAuthn.getCredential', _params)
|
|
137
|
+
return GetCredentialReturn.from_json(_result)
|
|
138
|
+
|
|
139
|
+
async def get_credentials(self, *, authenticator_id: AuthenticatorId) -> GetCredentialsReturn:
|
|
140
|
+
"""
|
|
141
|
+
Returns all the credentials stored in the given virtual authenticator.
|
|
142
|
+
:param authenticator_id:
|
|
143
|
+
"""
|
|
144
|
+
_params: Dict[str, Any] = {}
|
|
145
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
146
|
+
_result = await self._target.send('WebAuthn.getCredentials', _params)
|
|
147
|
+
return GetCredentialsReturn.from_json(_result)
|
|
148
|
+
|
|
149
|
+
async def remove_credential(self, *, authenticator_id: AuthenticatorId, credential_id: str) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Removes a credential from the authenticator.
|
|
152
|
+
:param authenticator_id:
|
|
153
|
+
:param credential_id:
|
|
154
|
+
"""
|
|
155
|
+
_params: Dict[str, Any] = {}
|
|
156
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
157
|
+
_params['credentialId'] = encode(FieldMeta('', '', False, 'primitive'), credential_id)
|
|
158
|
+
_result = await self._target.send('WebAuthn.removeCredential', _params)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
async def clear_credentials(self, *, authenticator_id: AuthenticatorId) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Clears all the credentials from the specified device.
|
|
164
|
+
:param authenticator_id:
|
|
165
|
+
"""
|
|
166
|
+
_params: Dict[str, Any] = {}
|
|
167
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
168
|
+
_result = await self._target.send('WebAuthn.clearCredentials', _params)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
async def set_user_verified(self, *, authenticator_id: AuthenticatorId, is_user_verified: bool) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Sets whether User Verification succeeds or fails for an authenticator.
|
|
174
|
+
The default is true.
|
|
175
|
+
:param authenticator_id:
|
|
176
|
+
:param is_user_verified:
|
|
177
|
+
"""
|
|
178
|
+
_params: Dict[str, Any] = {}
|
|
179
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
180
|
+
_params['isUserVerified'] = encode(FieldMeta('', '', False, 'primitive'), is_user_verified)
|
|
181
|
+
_result = await self._target.send('WebAuthn.setUserVerified', _params)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
async def set_automatic_presence_simulation(self, *, authenticator_id: AuthenticatorId, enabled: bool) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator.
|
|
187
|
+
The default is true.
|
|
188
|
+
:param authenticator_id:
|
|
189
|
+
:param enabled:
|
|
190
|
+
"""
|
|
191
|
+
_params: Dict[str, Any] = {}
|
|
192
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
193
|
+
_params['enabled'] = encode(FieldMeta('', '', False, 'primitive'), enabled)
|
|
194
|
+
_result = await self._target.send('WebAuthn.setAutomaticPresenceSimulation', _params)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
async def set_credential_properties(self, *, authenticator_id: AuthenticatorId, credential_id: str, backup_eligibility: Optional[bool] = None, backup_state: Optional[bool] = None) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Allows setting credential properties.
|
|
200
|
+
https://w3c.github.io/webauthn/#sctn-automation-set-credential-properties
|
|
201
|
+
:param authenticator_id:
|
|
202
|
+
:param credential_id:
|
|
203
|
+
:param backup_eligibility:
|
|
204
|
+
:param backup_state:
|
|
205
|
+
"""
|
|
206
|
+
_params: Dict[str, Any] = {}
|
|
207
|
+
_params['authenticatorId'] = encode(FieldMeta('', '', False, 'primitive'), authenticator_id)
|
|
208
|
+
_params['credentialId'] = encode(FieldMeta('', '', False, 'primitive'), credential_id)
|
|
209
|
+
if backup_eligibility is not None:
|
|
210
|
+
_params['backupEligibility'] = encode(FieldMeta('', '', False, 'primitive'), backup_eligibility)
|
|
211
|
+
if backup_state is not None:
|
|
212
|
+
_params['backupState'] = encode(FieldMeta('', '', False, 'primitive'), backup_state)
|
|
213
|
+
_result = await self._target.send('WebAuthn.setCredentialProperties', _params)
|
|
214
|
+
return None
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Custom types and enums for the WebAuthn domain (generated)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, ClassVar, Dict, List, Literal, Optional
|
|
7
|
+
|
|
8
|
+
from ..mixins.datatype import DataType, FieldMeta, register
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
type AuthenticatorId = str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register("WebAuthn.AuthenticatorProtocol")
|
|
15
|
+
class AuthenticatorProtocol(str, Enum):
|
|
16
|
+
U2F = 'u2f'
|
|
17
|
+
CTAP2 = 'ctap2'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register("WebAuthn.Ctap2Version")
|
|
21
|
+
class Ctap2Version(str, Enum):
|
|
22
|
+
CTAP2_0 = 'ctap2_0'
|
|
23
|
+
CTAP2_1 = 'ctap2_1'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register("WebAuthn.AuthenticatorTransport")
|
|
27
|
+
class AuthenticatorTransport(str, Enum):
|
|
28
|
+
USB = 'usb'
|
|
29
|
+
NFC = 'nfc'
|
|
30
|
+
BLE = 'ble'
|
|
31
|
+
CABLE = 'cable'
|
|
32
|
+
INTERNAL = 'internal'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@register("WebAuthn.VirtualAuthenticatorOptions")
|
|
36
|
+
@dataclass
|
|
37
|
+
class VirtualAuthenticatorOptions(DataType):
|
|
38
|
+
protocol: AuthenticatorProtocol
|
|
39
|
+
transport: AuthenticatorTransport
|
|
40
|
+
ctap2_version: Optional[Ctap2Version] = None
|
|
41
|
+
has_resident_key: Optional[bool] = None
|
|
42
|
+
has_user_verification: Optional[bool] = None
|
|
43
|
+
has_large_blob: Optional[bool] = None
|
|
44
|
+
has_cred_blob: Optional[bool] = None
|
|
45
|
+
has_min_pin_length: Optional[bool] = None
|
|
46
|
+
has_prf: Optional[bool] = None
|
|
47
|
+
automatic_presence_simulation: Optional[bool] = None
|
|
48
|
+
is_user_verified: Optional[bool] = None
|
|
49
|
+
default_backup_eligibility: Optional[bool] = None
|
|
50
|
+
default_backup_state: Optional[bool] = None
|
|
51
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
52
|
+
FieldMeta('protocol', 'protocol', False, 'enum', ref='WebAuthn.AuthenticatorProtocol'),
|
|
53
|
+
FieldMeta('transport', 'transport', False, 'enum', ref='WebAuthn.AuthenticatorTransport'),
|
|
54
|
+
FieldMeta('ctap2_version', 'ctap2Version', True, 'enum', ref='WebAuthn.Ctap2Version'),
|
|
55
|
+
FieldMeta('has_resident_key', 'hasResidentKey', True, 'primitive'),
|
|
56
|
+
FieldMeta('has_user_verification', 'hasUserVerification', True, 'primitive'),
|
|
57
|
+
FieldMeta('has_large_blob', 'hasLargeBlob', True, 'primitive'),
|
|
58
|
+
FieldMeta('has_cred_blob', 'hasCredBlob', True, 'primitive'),
|
|
59
|
+
FieldMeta('has_min_pin_length', 'hasMinPinLength', True, 'primitive'),
|
|
60
|
+
FieldMeta('has_prf', 'hasPrf', True, 'primitive'),
|
|
61
|
+
FieldMeta('automatic_presence_simulation', 'automaticPresenceSimulation', True, 'primitive'),
|
|
62
|
+
FieldMeta('is_user_verified', 'isUserVerified', True, 'primitive'),
|
|
63
|
+
FieldMeta('default_backup_eligibility', 'defaultBackupEligibility', True, 'primitive'),
|
|
64
|
+
FieldMeta('default_backup_state', 'defaultBackupState', True, 'primitive'),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@register("WebAuthn.Credential")
|
|
69
|
+
@dataclass
|
|
70
|
+
class Credential(DataType):
|
|
71
|
+
credential_id: str
|
|
72
|
+
is_resident_credential: bool
|
|
73
|
+
private_key: str
|
|
74
|
+
sign_count: int
|
|
75
|
+
rp_id: Optional[str] = None
|
|
76
|
+
user_handle: Optional[str] = None
|
|
77
|
+
large_blob: Optional[str] = None
|
|
78
|
+
backup_eligibility: Optional[bool] = None
|
|
79
|
+
backup_state: Optional[bool] = None
|
|
80
|
+
user_name: Optional[str] = None
|
|
81
|
+
user_display_name: Optional[str] = None
|
|
82
|
+
__FIELDS__: ClassVar[tuple] = (
|
|
83
|
+
FieldMeta('credential_id', 'credentialId', False, 'primitive'),
|
|
84
|
+
FieldMeta('is_resident_credential', 'isResidentCredential', False, 'primitive'),
|
|
85
|
+
FieldMeta('private_key', 'privateKey', False, 'primitive'),
|
|
86
|
+
FieldMeta('sign_count', 'signCount', False, 'primitive'),
|
|
87
|
+
FieldMeta('rp_id', 'rpId', True, 'primitive'),
|
|
88
|
+
FieldMeta('user_handle', 'userHandle', True, 'primitive'),
|
|
89
|
+
FieldMeta('large_blob', 'largeBlob', True, 'primitive'),
|
|
90
|
+
FieldMeta('backup_eligibility', 'backupEligibility', True, 'primitive'),
|
|
91
|
+
FieldMeta('backup_state', 'backupState', True, 'primitive'),
|
|
92
|
+
FieldMeta('user_name', 'userName', True, 'primitive'),
|
|
93
|
+
FieldMeta('user_display_name', 'userDisplayName', True, 'primitive'),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
__all__ = ["AuthenticatorId", "AuthenticatorProtocol", "AuthenticatorTransport", "Credential", "Ctap2Version", "VirtualAuthenticatorOptions"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Core runtime: connection, targets, browser, page and request tracking."""
|
|
2
|
+
|
|
3
|
+
from .browser import Browser
|
|
4
|
+
from .browser_context import BrowserContext
|
|
5
|
+
from .element import Element
|
|
6
|
+
from .feature import Feature
|
|
7
|
+
from .page import Page
|
|
8
|
+
from .target import ProtocolError, Target
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Browser",
|
|
12
|
+
"BrowserContext",
|
|
13
|
+
"Element",
|
|
14
|
+
"Feature",
|
|
15
|
+
"Page",
|
|
16
|
+
"ProtocolError",
|
|
17
|
+
"Target",
|
|
18
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Browser-level target: the websocket endpoint and target management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import ClassVar, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..cdp import CDP
|
|
12
|
+
from ..cdp import Target as TargetDomain
|
|
13
|
+
from .browser_context import BrowserContext
|
|
14
|
+
from .pagable import PagableTarget
|
|
15
|
+
from .page import Page
|
|
16
|
+
from .target import CDPConnection, Target, cdp_host, cdp_target_path, ws_origin
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Browser[T: Page](Target, PagableTarget[T]):
|
|
20
|
+
"""The browser endpoint.
|
|
21
|
+
|
|
22
|
+
Connects to the browser-level websocket, discovers page targets and exposes
|
|
23
|
+
them as :class:`Page` objects -- each on its own websocket
|
|
24
|
+
(``{host}/devtools/page/{id}``), with the host kept in the :data:`cdp_host`
|
|
25
|
+
context variable. Pages are grouped into :class:`BrowserContext` profiles;
|
|
26
|
+
the default context holds pages created without an explicit one.
|
|
27
|
+
|
|
28
|
+
Generic over the page class: ``page_class`` drives ``T``, so
|
|
29
|
+
``browser.pages``, ``new_page()`` and contexts are typed to the page type
|
|
30
|
+
you pass (e.g. ``Browser.connect_http(url, page_class=Page[RequestListener])``).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
#: Page class captured from ``Browser[Page[...]]`` subscription, used by
|
|
34
|
+
#: :meth:`get_distant_browser` so the feature set is derived automatically.
|
|
35
|
+
_page_class_override: ClassVar[Optional[type]] = None
|
|
36
|
+
|
|
37
|
+
def __class_getitem__(cls, item):
|
|
38
|
+
"""``Browser[Page[Feat]]`` -> a real subclass remembering that page class.
|
|
39
|
+
|
|
40
|
+
A bare type parameter (``Browser[P]`` inside generic code) falls back to
|
|
41
|
+
the normal typing machinery.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(item, type) and issubclass(item, Page):
|
|
44
|
+
name = f"{cls.__name__}[{item.__name__}]"
|
|
45
|
+
return type(name, (cls,), {"_page_class_override": item})
|
|
46
|
+
return super().__class_getitem__(item) # type: ignore[misc]
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
target_info: TargetDomain.TargetInfo,
|
|
51
|
+
*,
|
|
52
|
+
page_class: type[T] = Page,
|
|
53
|
+
):
|
|
54
|
+
Target.__init__(self, target_info, None)
|
|
55
|
+
PagableTarget.__init__(self)
|
|
56
|
+
self.page_class: type[T] = page_class
|
|
57
|
+
self._contexts: dict[Optional[str], BrowserContext[T]] = {}
|
|
58
|
+
self.default_context: BrowserContext[T] | None = None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def contexts(self):
|
|
62
|
+
return list(self._contexts.values())
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def _scope_context_id(self) -> Optional[str]:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
async def connect_http[P: Page = Page](
|
|
70
|
+
cls,
|
|
71
|
+
endpoint: str = "http://127.0.0.1:9222",
|
|
72
|
+
*,
|
|
73
|
+
page_class: type[P] = Page,
|
|
74
|
+
) -> "Browser[P]":
|
|
75
|
+
"""Discover the websocket via ``/json/version`` then connect.
|
|
76
|
+
|
|
77
|
+
Binds :data:`cdp_host` to the endpoint's origin so every page connected
|
|
78
|
+
afterwards reaches the browser at the same host without it being passed
|
|
79
|
+
around.
|
|
80
|
+
"""
|
|
81
|
+
with urllib.request.urlopen(f"{endpoint.rstrip('/')}/json/version") as resp:
|
|
82
|
+
info = json.loads(resp.read())
|
|
83
|
+
ws_url = info["webSocketDebuggerUrl"]
|
|
84
|
+
cdp_host.set(ws_origin(ws_url))
|
|
85
|
+
|
|
86
|
+
boot = CDPConnection()
|
|
87
|
+
await boot._open(ws_url)
|
|
88
|
+
try:
|
|
89
|
+
browser_info = (await CDP(boot).Target.get_target_info()).target_info
|
|
90
|
+
finally:
|
|
91
|
+
await boot.close()
|
|
92
|
+
|
|
93
|
+
browser = Browser[P](browser_info, page_class=page_class)
|
|
94
|
+
await browser.connect(ws_url)
|
|
95
|
+
await browser._discover()
|
|
96
|
+
return browser
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
async def get_distant_browser(
|
|
100
|
+
cls,
|
|
101
|
+
server: str = "http://127.0.0.1:9333",
|
|
102
|
+
*,
|
|
103
|
+
features: Optional[tuple] = None,
|
|
104
|
+
page_class: Optional[type] = None,
|
|
105
|
+
**launch,
|
|
106
|
+
) -> "Browser[T]":
|
|
107
|
+
"""Create a browser on a parsek-cdp-server and return it, connected.
|
|
108
|
+
|
|
109
|
+
``Browser[Page[RequestListener]].get_distant_browser("http://host:9333",
|
|
110
|
+
headless=True)`` issues ``POST /browsers?feature=RequestListener`` with the
|
|
111
|
+
launch settings (``headless``, ``executable``, ``extra_args``, ...) in the
|
|
112
|
+
body, then connects to the proxy's control channel. The returned browser
|
|
113
|
+
drives the *remote* browser exactly like a local one -- pages connect to
|
|
114
|
+
the proxy's per-target endpoints and the named features are aggregated
|
|
115
|
+
server-side.
|
|
116
|
+
|
|
117
|
+
The feature set defaults to the ones declared on the subscripted page
|
|
118
|
+
class (``Page[RequestListener]`` -> ``RequestListener``); pass ``features``
|
|
119
|
+
to override.
|
|
120
|
+
"""
|
|
121
|
+
resolved_page = page_class or cls._page_class_override or Page
|
|
122
|
+
feats = (
|
|
123
|
+
features
|
|
124
|
+
if features is not None
|
|
125
|
+
else getattr(resolved_page, "_feature_classes", ())
|
|
126
|
+
)
|
|
127
|
+
query = "&".join(f"feature={urllib.parse.quote(f.__name__)}" for f in feats)
|
|
128
|
+
url = f"{server.rstrip('/')}/browsers" + (f"?{query}" if query else "")
|
|
129
|
+
payload = json.dumps(launch).encode()
|
|
130
|
+
# Run the blocking HTTP POST off the event loop so it never stalls it
|
|
131
|
+
# (notably when client and server share one loop in tests).
|
|
132
|
+
info = await asyncio.to_thread(cls._post_json, url, payload)
|
|
133
|
+
|
|
134
|
+
control_url = info["wsUrl"]
|
|
135
|
+
suffix = "/control"
|
|
136
|
+
ws_base = (
|
|
137
|
+
control_url[: -len(suffix)] if control_url.endswith(suffix) else control_url
|
|
138
|
+
)
|
|
139
|
+
cdp_host.set(ws_base)
|
|
140
|
+
cdp_target_path.set("/page/")
|
|
141
|
+
|
|
142
|
+
boot = CDPConnection()
|
|
143
|
+
await boot._open(control_url)
|
|
144
|
+
try:
|
|
145
|
+
browser_info = (await CDP(boot).Target.get_target_info()).target_info
|
|
146
|
+
finally:
|
|
147
|
+
await boot.close()
|
|
148
|
+
|
|
149
|
+
# Pages on a remote browser run their features as *clients* (consuming the
|
|
150
|
+
# server-aggregated ``Parsek.*`` events), not "local" (which would digest
|
|
151
|
+
# raw CDP -- exactly what the server already suppresses).
|
|
152
|
+
remote_page = type(
|
|
153
|
+
resolved_page.__name__,
|
|
154
|
+
(resolved_page,),
|
|
155
|
+
{"_feature_role": "client"},
|
|
156
|
+
)
|
|
157
|
+
browser = cls(browser_info, page_class=remote_page)
|
|
158
|
+
await browser.connect(control_url)
|
|
159
|
+
await browser._discover()
|
|
160
|
+
return browser
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _post_json(url: str, payload: bytes) -> dict:
|
|
164
|
+
request = urllib.request.Request(
|
|
165
|
+
url,
|
|
166
|
+
data=payload,
|
|
167
|
+
method="POST",
|
|
168
|
+
headers={"Content-Type": "application/json"},
|
|
169
|
+
)
|
|
170
|
+
with urllib.request.urlopen(request) as resp:
|
|
171
|
+
return json.loads(resp.read())
|
|
172
|
+
|
|
173
|
+
async def _discover(self) -> None:
|
|
174
|
+
"""Populate contexts and pages from the live browser (after connect)."""
|
|
175
|
+
contexts = await self.cdp.Target.get_browser_contexts()
|
|
176
|
+
for context_id in contexts.browser_context_ids:
|
|
177
|
+
self._contexts[context_id] = BrowserContext(self, context_id)
|
|
178
|
+
for target in self.targets:
|
|
179
|
+
target_data = target._target
|
|
180
|
+
if target_data.type_ not in ("page", "tab"):
|
|
181
|
+
continue
|
|
182
|
+
page = await self._ensure_page(target_data)
|
|
183
|
+
self._pages[target.id] = page
|
|
184
|
+
if target_data.browser_context_id not in self._contexts:
|
|
185
|
+
context = BrowserContext(self, target_data.browser_context_id)
|
|
186
|
+
self._contexts[target_data.browser_context_id] = context
|
|
187
|
+
self.default_context = context
|
|
188
|
+
self._contexts[target_data.browser_context_id]._pages[target.id] = page
|
|
189
|
+
|
|
190
|
+
async def connect(self, url: Optional[str] = None) -> "Browser[T]":
|
|
191
|
+
"""Connect the browser websocket and start watching for page targets.
|
|
192
|
+
|
|
193
|
+
The browser endpoint's ws path id does not match its ``target_id``, so
|
|
194
|
+
:meth:`connect_http` passes the discovered ``webSocketDebuggerUrl`` as
|
|
195
|
+
``url``. Generic target tracking is wired by :meth:`Target.connect`; the
|
|
196
|
+
page-specific discovery handling lives in :class:`PagableTarget` and is
|
|
197
|
+
registered here on the browser's own connection.
|
|
198
|
+
"""
|
|
199
|
+
await super().connect(url)
|
|
200
|
+
await self.cdp.Target.set_discover_targets(discover=True)
|
|
201
|
+
await self.refresh_targets()
|
|
202
|
+
self.watch(self)
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
async def create_context(
|
|
206
|
+
self,
|
|
207
|
+
*,
|
|
208
|
+
dispose_on_detach: Optional[bool] = None,
|
|
209
|
+
proxy_server: Optional[str] = None,
|
|
210
|
+
proxy_bypass_list: Optional[str] = None,
|
|
211
|
+
):
|
|
212
|
+
"""Create a fresh, isolated browser context (incognito-like)."""
|
|
213
|
+
result = await self.cdp.Target.create_browser_context(
|
|
214
|
+
dispose_on_detach=dispose_on_detach,
|
|
215
|
+
proxy_server=proxy_server,
|
|
216
|
+
proxy_bypass_list=proxy_bypass_list,
|
|
217
|
+
)
|
|
218
|
+
ctx = BrowserContext[T](self, result.browser_context_id)
|
|
219
|
+
self._contexts[ctx.id] = ctx
|
|
220
|
+
return ctx
|
|
221
|
+
|
|
222
|
+
async def fetch_contexts(self) -> List[BrowserContext]:
|
|
223
|
+
"""Sync :attr:`contexts` with the browser and return them all."""
|
|
224
|
+
result = await self.cdp.Target.get_browser_contexts()
|
|
225
|
+
for cid in result.browser_context_ids:
|
|
226
|
+
self._contexts.setdefault(cid, BrowserContext(self, cid))
|
|
227
|
+
return list(self._contexts.values())
|
|
228
|
+
|
|
229
|
+
def _context_for(self, context_id: Optional[str]) -> BrowserContext[T]:
|
|
230
|
+
ctx = self._contexts.get(context_id)
|
|
231
|
+
if ctx is None:
|
|
232
|
+
ctx = BrowserContext(self, context_id)
|
|
233
|
+
self._contexts[context_id] = ctx
|
|
234
|
+
return ctx
|
|
235
|
+
|
|
236
|
+
def _forget_context(self, context_id: str) -> None:
|
|
237
|
+
ctx = self._contexts.pop(context_id, None)
|
|
238
|
+
if ctx is not None:
|
|
239
|
+
for target_id in list(ctx._pages):
|
|
240
|
+
self._drop_page(target_id)
|