mithwire 0.50.3__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.
Files changed (76) hide show
  1. mithwire/__init__.py +32 -0
  2. mithwire/cdp/README.md +4 -0
  3. mithwire/cdp/__init__.py +6 -0
  4. mithwire/cdp/accessibility.py +668 -0
  5. mithwire/cdp/animation.py +494 -0
  6. mithwire/cdp/audits.py +1995 -0
  7. mithwire/cdp/autofill.py +292 -0
  8. mithwire/cdp/background_service.py +215 -0
  9. mithwire/cdp/bluetooth_emulation.py +626 -0
  10. mithwire/cdp/browser.py +821 -0
  11. mithwire/cdp/cache_storage.py +311 -0
  12. mithwire/cdp/cast.py +172 -0
  13. mithwire/cdp/console.py +107 -0
  14. mithwire/cdp/crash_report_context.py +55 -0
  15. mithwire/cdp/css.py +2750 -0
  16. mithwire/cdp/database.py +179 -0
  17. mithwire/cdp/debugger.py +1405 -0
  18. mithwire/cdp/device_access.py +141 -0
  19. mithwire/cdp/device_orientation.py +45 -0
  20. mithwire/cdp/dom.py +2257 -0
  21. mithwire/cdp/dom_debugger.py +321 -0
  22. mithwire/cdp/dom_snapshot.py +876 -0
  23. mithwire/cdp/dom_storage.py +222 -0
  24. mithwire/cdp/emulation.py +1779 -0
  25. mithwire/cdp/event_breakpoints.py +56 -0
  26. mithwire/cdp/extensions.py +238 -0
  27. mithwire/cdp/fed_cm.py +283 -0
  28. mithwire/cdp/fetch.py +507 -0
  29. mithwire/cdp/file_system.py +115 -0
  30. mithwire/cdp/headless_experimental.py +115 -0
  31. mithwire/cdp/heap_profiler.py +401 -0
  32. mithwire/cdp/indexed_db.py +528 -0
  33. mithwire/cdp/input_.py +701 -0
  34. mithwire/cdp/inspector.py +95 -0
  35. mithwire/cdp/io.py +101 -0
  36. mithwire/cdp/layer_tree.py +464 -0
  37. mithwire/cdp/log.py +190 -0
  38. mithwire/cdp/media.py +313 -0
  39. mithwire/cdp/memory.py +305 -0
  40. mithwire/cdp/network.py +5342 -0
  41. mithwire/cdp/overlay.py +1468 -0
  42. mithwire/cdp/page.py +3972 -0
  43. mithwire/cdp/performance.py +124 -0
  44. mithwire/cdp/performance_timeline.py +200 -0
  45. mithwire/cdp/preload.py +575 -0
  46. mithwire/cdp/profiler.py +420 -0
  47. mithwire/cdp/pwa.py +278 -0
  48. mithwire/cdp/py.typed +0 -0
  49. mithwire/cdp/runtime.py +1589 -0
  50. mithwire/cdp/schema.py +50 -0
  51. mithwire/cdp/security.py +518 -0
  52. mithwire/cdp/service_worker.py +401 -0
  53. mithwire/cdp/smart_card_emulation.py +891 -0
  54. mithwire/cdp/storage.py +1573 -0
  55. mithwire/cdp/system_info.py +327 -0
  56. mithwire/cdp/target.py +829 -0
  57. mithwire/cdp/tethering.py +65 -0
  58. mithwire/cdp/tracing.py +377 -0
  59. mithwire/cdp/util.py +18 -0
  60. mithwire/cdp/web_audio.py +606 -0
  61. mithwire/cdp/web_authn.py +598 -0
  62. mithwire/cdp/web_mcp.py +293 -0
  63. mithwire/core/_contradict.py +142 -0
  64. mithwire/core/browser.py +923 -0
  65. mithwire/core/cf_templates/cf_dark_checkbox.png +0 -0
  66. mithwire/core/cf_templates/cf_light_checkbox.png +0 -0
  67. mithwire/core/config.py +323 -0
  68. mithwire/core/connection.py +564 -0
  69. mithwire/core/element.py +1205 -0
  70. mithwire/core/tab.py +2202 -0
  71. mithwire/core/util.py +5063 -0
  72. mithwire-0.50.3.dist-info/METADATA +1049 -0
  73. mithwire-0.50.3.dist-info/RECORD +76 -0
  74. mithwire-0.50.3.dist-info/WHEEL +5 -0
  75. mithwire-0.50.3.dist-info/licenses/LICENSE.txt +619 -0
  76. mithwire-0.50.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,923 @@
1
+ # Copyright 2024 by UltrafunkAmsterdam (https://github.com/UltrafunkAmsterdam)
2
+ # All rights reserved.
3
+ # This file is part of the mithwire package.
4
+ # and is released under the "GNU AFFERO GENERAL PUBLIC LICENSE".
5
+ # Please see the LICENSE.txt file that should have been included as part of this package.
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import atexit
11
+ import http.cookiejar
12
+ import json
13
+ import logging
14
+ import os
15
+ import pathlib
16
+ import pickle
17
+ import urllib.parse
18
+ import urllib.request
19
+ import warnings
20
+ from collections import defaultdict
21
+ from typing import List, Tuple, Union
22
+
23
+ from .. import cdp
24
+ from . import tab, util
25
+ from ._contradict import ContraDict
26
+ from .config import Config, PathLike, is_posix
27
+ from .connection import Connection
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class Browser(Connection):
33
+ """
34
+ The Browser object is the "root" of the hierarchy and contains a reference
35
+ to the browser parent process.
36
+ there should usually be only 1 instance of this.
37
+
38
+ All opened tabs, extra browser screens and resources will not cause a new Browser process,
39
+ but rather create additional :class:`mithwire.Tab` objects.
40
+
41
+ So, besides starting your instance and first/additional tabs, you don't actively use it a lot under normal conditions.
42
+
43
+ Tab objects will represent and control
44
+ - tabs (as you know them)
45
+ - browser windows (new window)
46
+ - iframe
47
+ - background processes
48
+
49
+ note:
50
+ the Browser object is not instantiated by __init__ but using the asynchronous :meth:`mithwire.Browser.create` method.
51
+
52
+ note:
53
+ in Chromium based browsers, there is a parent process which keeps running all the time, even if
54
+ there are no visible browser windows. sometimes it's stubborn to close it, so make sure after using
55
+ this library, the browser is correctly and fully closed/exited/killed.
56
+
57
+ """
58
+
59
+ _process: asyncio.subprocess.Process
60
+ _process_pid: int
61
+ _http: HTTPApi = None
62
+ _cookies: CookieJar = None
63
+
64
+ config: Config
65
+
66
+ @classmethod
67
+ async def create(
68
+ cls,
69
+ config: Config = None,
70
+ *,
71
+ user_data_dir: PathLike = None,
72
+ headless: bool = False,
73
+ browser_executable_path: PathLike = None,
74
+ browser_args: List[str] = None,
75
+ sandbox: bool = True,
76
+ host: str = None,
77
+ port: int = None,
78
+ **kwargs,
79
+ ) -> Browser:
80
+ """
81
+ entry point for creating an instance
82
+ """
83
+ if not config:
84
+ config = Config(
85
+ user_data_dir=user_data_dir,
86
+ headless=headless,
87
+ browser_executable_path=browser_executable_path,
88
+ browser_args=browser_args or [],
89
+ sandbox=sandbox,
90
+ host=host,
91
+ port=port,
92
+ **kwargs,
93
+ )
94
+ instance = cls(config)
95
+ await instance.start()
96
+ return instance
97
+
98
+ def __init__(self, config: Config, **kwargs):
99
+ """
100
+ constructor. to create a instance, use :py:meth:`Browser.create(...)`
101
+
102
+ :param config:
103
+ """
104
+
105
+ try:
106
+ asyncio.get_running_loop()
107
+ except RuntimeError:
108
+ raise RuntimeError(
109
+ "{0} objects of this class are created using await {0}.create()".format(
110
+ self.__class__.__name__
111
+ )
112
+ )
113
+ # weakref.finalize(self, self._quit, self)
114
+ self.config = config
115
+
116
+ """current targets (all types"""
117
+ self.info = None
118
+ self._target = None
119
+ self._process = None
120
+ self._process_pid = None
121
+ self._keep_user_data_dir = None
122
+ self._is_updating = asyncio.Event()
123
+ self.connection: Connection = None
124
+ super().__init__("", auto_attach=False)
125
+ logger.debug("Session object initialized: %s" % vars(self))
126
+
127
+ @property
128
+ def main_tab(self) -> tab.Tab:
129
+ """returns the target which was launched with the browser"""
130
+ return next(filter(lambda x: x.target.type_ == "page", self.targets))
131
+
132
+ @property
133
+ def targets(self) -> List[Connection]:
134
+ return self._targets
135
+
136
+ @property
137
+ def tabs(self) -> List[tab.Connection]:
138
+ return [x for x in self._targets if x.target.type_ == "page"]
139
+
140
+ # @property
141
+ # def tabs(self) -> List[tab.Tab]:
142
+ # """returns the current targets which are of type "page"
143
+ # :return:
144
+ # """
145
+ # tabs = filter(lambda item: item.type_ == "page", self.targets)
146
+ # return [tab.Tab(self, x) for x in tabs]
147
+ # # return list(tabs)
148
+
149
+ @property
150
+ def cookies(self) -> CookieJar:
151
+ if not self._cookies:
152
+ self._cookies = CookieJar(self)
153
+ return self._cookies
154
+
155
+ @property
156
+ def stopped(self):
157
+ if self._process and self._process.returncode is None:
158
+ return False
159
+ return True
160
+ # return (self._process and self._process.returncode) or False
161
+
162
+ async def wait(self, time: Union[float, int] = 0.1):
163
+ """wait for <time> seconds. important to use, especially in between page navigation
164
+
165
+ :param time:
166
+ :return:
167
+ """
168
+ try:
169
+ await asyncio.wait(
170
+ [
171
+ asyncio.create_task(self.update_targets()),
172
+ asyncio.create_task(asyncio.sleep(time)),
173
+ ],
174
+ return_when=asyncio.ALL_COMPLETED,
175
+ )
176
+ except asyncio.TimeoutError:
177
+ pass
178
+
179
+ sleep = wait
180
+ """alias for wait"""
181
+
182
+ async def get(
183
+ self, url="chrome://welcome", new_tab: bool = False, new_window: bool = False
184
+ ) -> tab.Tab:
185
+ """top level get. utilizes the first tab to retrieve given url.
186
+
187
+ convenience function known from selenium.
188
+ this function handles waits/sleeps and detects when DOM events fired, so it's the safest
189
+ way of navigating.
190
+
191
+ :param url: the url to navigate to
192
+ :param new_tab: open new tab
193
+ :param new_window: open new window
194
+ :return: Page
195
+ """
196
+ if new_tab or new_window:
197
+ # creat new target using the browser session
198
+ target_id = await self.send(
199
+ cdp.target.create_target(
200
+ url, new_window=new_window, enable_begin_frame_control=True
201
+ )
202
+ )
203
+
204
+ # connection = tab.Tab(target=target_id, parent=self, auto_attach=False)
205
+ # await connection.attach()
206
+ # self._targets.append(connection)
207
+ await self.update_targets()
208
+ connection = next(
209
+ filter(lambda x: x.target.target_id == target_id, self.targets)
210
+ )
211
+ await connection.attach()
212
+
213
+ else:
214
+ # first tab from browser.tabs
215
+ connection: tab.Tab = next(
216
+ filter(lambda item: item.target.type_ == "page", self.targets)
217
+ )
218
+
219
+ frame_id, loader_id, *_ = await connection.send(cdp.page.navigate(url))
220
+ await connection.attach()
221
+ await self.update_targets()
222
+ # await self
223
+ return connection
224
+
225
+ async def create_context(
226
+ self,
227
+ url: str = "chrome://welcome",
228
+ new_tab: bool = False,
229
+ new_window: bool = True,
230
+ dispose_on_detach: bool = True,
231
+ proxy_server: str = None,
232
+ proxy_bypass_list: List[str] = None,
233
+ origins_with_universal_network_access: List[str] = None,
234
+ proxy_ssl_context=None,
235
+ ) -> tab.Tab:
236
+ """
237
+ creates a new browser context - mostly useful if you want to use proxies for different browser instances
238
+ since chrome usually can only use 1 proxy per browser.
239
+ socks5 with authentication is supported by using a forwarder proxy, the
240
+ correct string to use socks proxy with username/password auth is socks://USERNAME:PASSWORD@SERVER:PORT
241
+ http/https proxies with authentication are also supported: http://USERNAME:PASSWORD@SERVER:PORT
242
+
243
+ dispose_on_detach – (EXPERIMENTAL) (Optional) If specified, disposes this context when debugging session disconnects.
244
+ proxy_server – (EXPERIMENTAL) (Optional) Proxy server, similar to the one passed to –proxy-server
245
+ proxy_bypass_list – (EXPERIMENTAL) (Optional) Proxy bypass list, similar to the one passed to –proxy-bypass-list
246
+ origins_with_universal_network_access – (EXPERIMENTAL) (Optional) An optional list of origins to grant unlimited cross-origin access to. Parts of the URL other than those constituting origin are ignored.
247
+ proxy_ssl_context – (Optional) Custom SSL context for HTTPS proxy connections. If None, a default context is used.
248
+
249
+ :param new_window:
250
+ :type new_window:
251
+ :param new_tab:
252
+ :type new_tab:
253
+ :param url:
254
+ :type url:
255
+ :param dispose_on_detach:
256
+ :type dispose_on_detach:
257
+ :param proxy_server:
258
+ :type proxy_server:
259
+ :param proxy_bypass_list:
260
+ :type proxy_bypass_list:
261
+ :param origins_with_universal_network_access:
262
+ :type origins_with_universal_network_access:
263
+ :param proxy_ssl_context:
264
+ :type proxy_ssl_context: ssl.SSLContext
265
+ :return:
266
+ :rtype:
267
+ """
268
+
269
+ if proxy_server:
270
+ fw = util.ProxyForwarder(
271
+ proxy_server=proxy_server, ssl_context=proxy_ssl_context
272
+ )
273
+ proxy_server = fw.proxy_server
274
+
275
+ ctx: cdp.browser.BrowserContextID = await self.send(
276
+ cdp.target.create_browser_context(
277
+ dispose_on_detach=dispose_on_detach,
278
+ proxy_server=proxy_server,
279
+ proxy_bypass_list=proxy_bypass_list,
280
+ origins_with_universal_network_access=origins_with_universal_network_access,
281
+ )
282
+ )
283
+ target_id: cdp.target.TargetID = await self.send(
284
+ cdp.target.create_target(
285
+ url, browser_context_id=ctx, new_window=new_window, for_tab=new_tab
286
+ )
287
+ )
288
+ await self.sleep(0.5)
289
+ connection: tab.Tab = next(
290
+ filter(
291
+ lambda item: item.target.type_ == "page"
292
+ and item.target.target_id == target_id,
293
+ self.targets,
294
+ )
295
+ )
296
+ return connection
297
+
298
+ async def start(self=None) -> Browser:
299
+ """launches the actual browser"""
300
+
301
+ if not self:
302
+ raise RuntimeError(
303
+ "use ``await Browser.create()`` to create a new instance"
304
+ )
305
+
306
+ if self._process or self._process_pid:
307
+ if self._process.returncode is not None:
308
+ return await self.create(config=self.config)
309
+ raise RuntimeError("ignored! this call has no effect when already running.")
310
+
311
+ # self.config.update(kwargs)
312
+ connect_existing = False
313
+ if self.config.host is not None and self.config.port is not None:
314
+ connect_existing = True
315
+ else:
316
+ self.config.host = "127.0.0.1"
317
+ self.config.port = util.free_port()
318
+
319
+ if not connect_existing:
320
+ logger.debug(
321
+ "BROWSER EXECUTABLE PATH: %s", self.config.browser_executable_path
322
+ )
323
+ if not pathlib.Path(self.config.browser_executable_path).exists():
324
+ raise FileNotFoundError(
325
+ (
326
+ """
327
+ ---------------------
328
+ Could not determine browser executable.
329
+ ---------------------
330
+ Make sure your browser is installed in the default location (path).
331
+ If you are sure about the browser executable, you can specify it using
332
+ the `browser_executable_path='{}` parameter."""
333
+ ).format(
334
+ "/path/to/browser/executable"
335
+ if is_posix
336
+ else "c:/path/to/your/browser.exe"
337
+ )
338
+ )
339
+
340
+ if getattr(self.config, "_extensions", None): # noqa
341
+ self.config.add_argument(
342
+ "--load-extension=%s"
343
+ % ",".join(str(_) for _ in self.config._extensions)
344
+ ) # noqa
345
+
346
+ exe = self.config.browser_executable_path
347
+ params = self.config()
348
+
349
+ logger.info(
350
+ "starting\n\texecutable :%s\n\narguments:\n%s", exe, "\n\t".join(params)
351
+ )
352
+ if not connect_existing:
353
+ self._process: asyncio.subprocess.Process = (
354
+ await asyncio.create_subprocess_exec(
355
+ # self.config.browser_executable_path,
356
+ # *cmdparams,
357
+ exe,
358
+ *params,
359
+ stdin=asyncio.subprocess.PIPE,
360
+ stdout=asyncio.subprocess.PIPE,
361
+ stderr=asyncio.subprocess.PIPE,
362
+ close_fds=is_posix,
363
+ )
364
+ )
365
+ self._process_pid = self._process.pid
366
+
367
+ self._http = HTTPApi((self.config.host, self.config.port))
368
+ util.get_registered_instances().add(self)
369
+ # Connect to the freshly-spawned Chrome's DevTools endpoint. The old
370
+ # loop budgeted only ~2.75s (5 attempts x 0.5s), which is tight: a
371
+ # cold Chrome typically binds its DevTools port in ~1-2s, leaving
372
+ # <1s of headroom. Any system hiccup (Spotlight scan, antivirus
373
+ # scan-on-execute, contended host, Chrome auto-update install) eats
374
+ # the margin and surfaces as a spurious "Failed to connect to
375
+ # browser" -- even though Chrome is fine and would have come up a
376
+ # moment later.
377
+ #
378
+ # Strategy: exponential backoff (50ms doubling, capped at 1s)
379
+ # against a wall-clock deadline of ~10s. Warm starts converge in a
380
+ # few probes; cold/contended starts get a real chance to finish;
381
+ # genuinely broken launches still fail in bounded time.
382
+ deadline = asyncio.get_running_loop().time() + 10.0
383
+ delay = 0.05
384
+ last_exc: BaseException | None = None
385
+ while True:
386
+ try:
387
+ self.info = ContraDict(await self._http.get("version"), silent=True)
388
+ break
389
+ except Exception as exc:
390
+ last_exc = exc
391
+ if asyncio.get_running_loop().time() + delay >= deadline:
392
+ logger.debug("could not start", exc_info=True)
393
+ break
394
+ await asyncio.sleep(delay)
395
+ delay = min(delay * 2, 1.0)
396
+
397
+ if not self.info:
398
+ raise Exception(
399
+ (
400
+ """
401
+ ---------------------
402
+ Failed to connect to browser
403
+ ---------------------
404
+ One of the causes could be when you are running as root.
405
+ In that case you need to pass no_sandbox=True
406
+ """
407
+ )
408
+ )
409
+
410
+ self.websocket_url = self.info.webSocketDebuggerUrl
411
+ await self.attach()
412
+ await self.update_targets()
413
+ # await self
414
+
415
+ async def grant_all_permissions(self):
416
+ """
417
+ grant permissions for:
418
+ accessibilityEvents
419
+ audioCapture
420
+ backgroundSync
421
+ backgroundFetch
422
+ clipboardReadWrite
423
+ clipboardSanitizedWrite
424
+ displayCapture
425
+ durableStorage
426
+ geolocation
427
+ idleDetection
428
+ localFonts
429
+ midi
430
+ midiSysex
431
+ nfc
432
+ notifications
433
+ paymentHandler
434
+ periodicBackgroundSync
435
+ protectedMediaIdentifier
436
+ sensors
437
+ storageAccess
438
+ topLevelStorageAccess
439
+ videoCapture
440
+ videoCapturePanTiltZoom
441
+ wakeLockScreen
442
+ wakeLockSystem
443
+ windowManagement
444
+ """
445
+ permissions = list(cdp.browser.PermissionType)
446
+ permissions.remove(cdp.browser.PermissionType.FLASH)
447
+ permissions.remove(cdp.browser.PermissionType.CAPTURED_SURFACE_CONTROL)
448
+ await self.send(cdp.browser.grant_permissions(permissions))
449
+
450
+ async def tile_windows(self, windows=None, max_columns: int = 0):
451
+ import math
452
+
453
+ import mss
454
+
455
+ m = mss.mss()
456
+ screen, screen_width, screen_height = 3 * (None,)
457
+ if m.monitors and len(m.monitors) >= 1:
458
+ screen = m.monitors[0]
459
+ screen_width = screen["width"]
460
+ screen_height = screen["height"]
461
+ if not screen or not screen_width or not screen_height:
462
+ warnings.warn("no monitors detected")
463
+ return
464
+ await self
465
+ distinct_windows = defaultdict(list)
466
+
467
+ if windows:
468
+ tabs = windows
469
+ else:
470
+ tabs = self.tabs
471
+ for tab in tabs:
472
+ window_id, bounds = await tab.get_window()
473
+ distinct_windows[window_id].append(tab)
474
+
475
+ num_windows = len(distinct_windows)
476
+ req_cols = max_columns or int(num_windows * (19 / 6))
477
+ req_rows = int(num_windows / req_cols)
478
+
479
+ while req_cols * req_rows < num_windows:
480
+ req_rows += 1
481
+
482
+ box_w = math.floor((screen_width / req_cols) - 1)
483
+ box_h = math.floor(screen_height / req_rows)
484
+
485
+ distinct_windows_iter = iter(distinct_windows.values())
486
+ grid = []
487
+ for x in range(req_cols):
488
+ for y in range(req_rows):
489
+ num = x + y
490
+ try:
491
+ tabs = next(distinct_windows_iter)
492
+ except StopIteration:
493
+ continue
494
+ if not tabs:
495
+ continue
496
+ tab = tabs[0]
497
+
498
+ try:
499
+ pos = [x * box_w, y * box_h, box_w, box_h]
500
+ grid.append(pos)
501
+ await tab.set_window_size(*pos)
502
+ except Exception:
503
+ logger.info(
504
+ "could not set window size. exception => ", exc_info=True
505
+ )
506
+ continue
507
+ return grid
508
+
509
+ async def _get_targets(self) -> List[cdp.target.TargetInfo]:
510
+ info = await self.send(cdp.target.get_targets())
511
+
512
+ return info
513
+
514
+ async def update_targets(self):
515
+
516
+ targets = await self.send(cdp.target.get_targets())
517
+ #
518
+ # current_tabs_targets = [t.target for t in self.children]
519
+ #
520
+ for t in targets:
521
+ for ctab in self._targets:
522
+ if ctab.target.target_id == t.target_id:
523
+ ctab.target = t
524
+ break
525
+ else:
526
+ _t = tab.Tab(target=t, parent=self, auto_attach=False)
527
+ self._targets.append(_t)
528
+
529
+ for ctab in self._targets.copy():
530
+ if ctab.target not in targets:
531
+ self._targets.remove(ctab)
532
+
533
+ await asyncio.sleep(0)
534
+
535
+ def __iter__(self):
536
+ self._i = self.tabs.index(self.main_tab)
537
+ return self
538
+
539
+ def __getitem__(
540
+ self, item: Union[str, int, slice]
541
+ ) -> Union[tab.Tab, List[tab.Tab], None]:
542
+ """
543
+ allows to get py:obj:`tab.Tab` instances by using browser[0], browser[1], etc.
544
+ a string is also allowed. it will then return the first tab where the py:obj:`cdp.target.TargetInfo` object
545
+ (as json string) contains the given key, or the first tab in case no matches are found. eg:
546
+ `browser["google"]` gives the first tab which has "google" in it's serialized target object.
547
+
548
+ :param item:
549
+ :type item:
550
+ :return:
551
+ :rtype: tab.Tab
552
+ """
553
+ if isinstance(item, int):
554
+ return self.tabs[item]
555
+ elif isinstance(item, slice):
556
+ tabs: List[tab.Tab] = []
557
+ sta, sto, ste = item.start, item.stop, item.step
558
+ if not ste:
559
+ ste = 1
560
+ if not sto:
561
+ sto = len(self.tabs) - 1
562
+ if not sta:
563
+ sta = 0
564
+ for x in range(sta, sto, ste):
565
+ try:
566
+ tabs.append(self.tabs[x])
567
+ except IndexError:
568
+ pass
569
+ return tabs
570
+ elif isinstance(item, tuple):
571
+ r = range(*item)
572
+ tabs: List[tab.Tab] = []
573
+ for i in r:
574
+ try:
575
+ tabs.append(self.tabs[i])
576
+ except IndexError:
577
+ pass
578
+ return tabs
579
+ elif isinstance(item, str):
580
+ for t in self.tabs:
581
+ if item.lower() in str(t.target.to_json()).lower():
582
+ return t
583
+ else:
584
+ return self.tabs[0]
585
+
586
+ def __reversed__(self):
587
+ return reversed(list(self.tabs))
588
+
589
+ def __next__(self) -> tab.Tab | tab.Connection | None:
590
+ try:
591
+ return self.tabs[self._i]
592
+ except IndexError:
593
+ del self._i
594
+ raise StopIteration
595
+ except AttributeError:
596
+ del self._i
597
+ raise StopIteration
598
+ finally:
599
+ if hasattr(self, "_i"):
600
+ if self._i != len(self.tabs):
601
+ self._i += 1
602
+ else:
603
+ del self._i
604
+
605
+ def stop(self):
606
+ try:
607
+ # asyncio.get_running_loop().create_task(self.send(cdp.browser.close()))
608
+
609
+ asyncio.get_event_loop().create_task(self.aclose())
610
+ logger.debug("closed the connection using get_event_loop().create_task()")
611
+ except RuntimeError:
612
+ if self.connection:
613
+ try:
614
+ # asyncio.run(self.send(cdp.browser.close()))
615
+ asyncio.run(self.aclose())
616
+ logger.debug("closed the connection using asyncio.run()")
617
+ except Exception:
618
+ pass
619
+
620
+ for _ in range(3):
621
+ try:
622
+ self._process.terminate()
623
+ logger.info(
624
+ "terminated browser with pid %d successfully" % self._process.pid
625
+ )
626
+ break
627
+ except (Exception,):
628
+ try:
629
+ self._process.kill()
630
+ logger.info(
631
+ "killed browser with pid %d successfully" % self._process.pid
632
+ )
633
+ break
634
+ except (Exception,):
635
+ try:
636
+ if hasattr(self, "browser_process_pid"):
637
+ os.kill(self._process_pid, 15)
638
+ logger.info(
639
+ "killed browser with pid %d using signal 15 successfully"
640
+ % self._process.pid
641
+ )
642
+ break
643
+ except (TypeError,):
644
+ logger.info("typerror", exc_info=True)
645
+ pass
646
+ except (PermissionError,):
647
+ logger.info(
648
+ "browser already stopped, or no permission to kill. skip"
649
+ )
650
+ pass
651
+ except (ProcessLookupError,):
652
+ logger.info("process lookup failure")
653
+ pass
654
+ except (Exception,):
655
+ raise
656
+ self._process = None
657
+ self._process_pid = None
658
+
659
+ def __await__(self):
660
+ # return ( asyncio.sleep(0)).__await__()
661
+ return self.update_targets().__await__()
662
+
663
+ def __del__(self):
664
+ pass
665
+
666
+
667
+ class CookieJar:
668
+ def __init__(self, browser: Browser):
669
+ self._browser = browser
670
+ # self._connection = connection
671
+
672
+ async def get_all(
673
+ self, requests_cookie_format: bool = False
674
+ ) -> List[Union[cdp.network.Cookie, "http.cookiejar.Cookie"]]:
675
+ """
676
+ get all cookies
677
+
678
+ :param requests_cookie_format: when True, returns python http.cookiejar.Cookie objects, compatible with requests library and many others.
679
+ :type requests_cookie_format: bool
680
+ :return:
681
+ :rtype:
682
+
683
+ """
684
+ connection = None
685
+ for tab in self._browser:
686
+ if tab.target:
687
+ connection = tab
688
+ break
689
+
690
+ else:
691
+ connection = self._browser
692
+ cookies = await connection.send(cdp.storage.get_cookies())
693
+ if requests_cookie_format:
694
+ import requests.cookies
695
+
696
+ return [
697
+ requests.cookies.create_cookie(
698
+ name=c.name,
699
+ value=c.value,
700
+ domain=c.domain,
701
+ path=c.path,
702
+ expires=c.expires,
703
+ secure=c.secure,
704
+ )
705
+ for c in cookies
706
+ ]
707
+ return cookies
708
+
709
+ async def set_all(self, cookies: List[cdp.network.CookieParam]):
710
+ """
711
+ set cookies
712
+
713
+ :param cookies: list of cookies
714
+ :type cookies:
715
+ :return:
716
+ :rtype:
717
+ """
718
+ connection = None
719
+ for tab in self._browser:
720
+ if tab.target:
721
+ connection = tab
722
+ break
723
+
724
+ else:
725
+ connection = self._browser
726
+
727
+ await connection.send(cdp.storage.set_cookies(cookies))
728
+
729
+ async def save(self, file: PathLike = ".session.dat", pattern: str = ".*"):
730
+ """
731
+ save all cookies (or a subset, controlled by `pattern`) to a file to be restored later
732
+
733
+ :param file:
734
+ :type file:
735
+ :param pattern: regex style pattern string.
736
+ any cookie that has a domain, key or value field which matches the pattern will be included.
737
+ default (param not specified) = ".*" (all)
738
+
739
+ eg: the pattern "(cf|.com|nowsecure)" will include those cookies which:
740
+ - have a string "cf" (cloudflare)
741
+ - have ".com" in them, in either domain, key or value field.
742
+ - contain "nowsecure"
743
+ :type pattern: str
744
+ :return:
745
+ :rtype:
746
+ """
747
+ import re
748
+
749
+ pattern = re.compile(pattern)
750
+ save_path = pathlib.Path(file).resolve()
751
+ connection = None
752
+ for tab in self._browser:
753
+ if tab.target:
754
+ connection = tab
755
+ break
756
+ else:
757
+ connection = self._browser
758
+
759
+ cookies = await self.get_all(requests_cookie_format=False)
760
+ included_cookies = []
761
+ for cookie in cookies:
762
+ for match in pattern.finditer(str(cookie.__dict__)):
763
+ logger.debug(
764
+ "saved cookie for matching pattern '%s' => (%s: %s)",
765
+ pattern.pattern,
766
+ cookie.name,
767
+ cookie.value,
768
+ )
769
+ included_cookies.append(cookie)
770
+ break
771
+ pickle.dump(cookies, save_path.open("w+b"))
772
+
773
+ async def load(self, file: PathLike = ".session.dat", pattern: str = ".*"):
774
+ """
775
+ load all cookies (or a subset, controlled by `pattern`) from a file created by :py:meth:`~save_cookies`.
776
+
777
+ :param file:
778
+ :type file:
779
+ :param pattern: regex style pattern string.
780
+ any cookie that has a domain, key or value field which matches the pattern will be included.
781
+ default (param not specified) = ".*" (all)
782
+
783
+ eg: the pattern "(cf|.com|nowsecure)" will include those cookies which:
784
+ - have a string "cf" (cloudflare)
785
+ - have ".com" in them, in either domain, key or value field.
786
+ - contain "nowsecure"
787
+ :type pattern: str
788
+ :return:
789
+ :rtype:
790
+ """
791
+ import re
792
+
793
+ pattern = re.compile(pattern)
794
+ save_path = pathlib.Path(file).resolve()
795
+ cookies = pickle.load(save_path.open("r+b"))
796
+ included_cookies = []
797
+ connection = None
798
+ for tab in self._browser:
799
+ if tab.target:
800
+ connection = tab
801
+ break
802
+
803
+ else:
804
+ connection = self._browser
805
+ for cookie in cookies:
806
+ for match in pattern.finditer(str(cookie.__dict__)):
807
+ included_cookies.append(cookie)
808
+ logger.debug(
809
+ "loaded cookie for matching pattern '%s' => (%s: %s)",
810
+ pattern.pattern,
811
+ cookie.name,
812
+ cookie.value,
813
+ )
814
+ break
815
+ await connection.send(cdp.storage.set_cookies(included_cookies))
816
+
817
+ async def clear(self):
818
+ """
819
+ clear current cookies
820
+
821
+ note: this includes all open tabs/windows for this browser
822
+
823
+ :return:
824
+ :rtype:
825
+ """
826
+ connection = None
827
+ for tab in self._browser:
828
+ if tab.target:
829
+ # continue
830
+ connection = tab
831
+ break
832
+
833
+ else:
834
+ connection = self._browser
835
+
836
+ await connection.send(cdp.storage.clear_cookies())
837
+
838
+
839
+ class HTTPApi:
840
+ def __init__(self, addr: Tuple[str, int]):
841
+ self.host, self.port = addr
842
+ self.api = "http://%s:%d" % (self.host, self.port)
843
+
844
+ @classmethod
845
+ def from_target(cls, target: "Target"):
846
+ ws_url = urllib.parse.urlparse(target.websocket_url)
847
+ inst = cls((ws_url.hostname, ws_url.port))
848
+ return inst
849
+
850
+ async def get(self, endpoint: str):
851
+ return await self._request(endpoint)
852
+
853
+ async def post(self, endpoint, data):
854
+ return await self._request(endpoint, data)
855
+
856
+ async def _request(self, endpoint, method: str = "get", data: dict = None):
857
+ url = urllib.parse.urljoin(
858
+ self.api, f"json/{endpoint}" if endpoint else "/json"
859
+ )
860
+ if data and method.lower() == "get":
861
+ raise ValueError("get requests cannot contain data")
862
+ if not url:
863
+ url = self.api + endpoint
864
+ request = urllib.request.Request(url)
865
+ request.method = method
866
+ request.data = None
867
+ if data:
868
+ request.data = json.dumps(data).encode("utf-8")
869
+
870
+ response = await asyncio.get_running_loop().run_in_executor(
871
+ None, lambda: urllib.request.urlopen(request, timeout=10)
872
+ )
873
+ return json.loads(response.read())
874
+
875
+
876
+ class BrowserContext:
877
+ def __init__(
878
+ self,
879
+ config: Config = None,
880
+ *,
881
+ user_data_dir: PathLike = None,
882
+ headless: bool = False,
883
+ browser_executable_path: PathLike = None,
884
+ browser_args: List[str] = None,
885
+ sandbox: bool = True,
886
+ host: str = None,
887
+ port: int = None,
888
+ keep_open: bool = False,
889
+ **kwargs,
890
+ ):
891
+ self._config = config
892
+ self._user_data_dir = user_data_dir
893
+ self._headless = headless
894
+ self._browser_executable_path = browser_executable_path
895
+ self._browser_args = browser_args
896
+ self._sandbox = sandbox
897
+ self._host = host
898
+ self._port = port
899
+ self._kwargs = kwargs
900
+ self._instance: Browser = None
901
+ self._keep_open = keep_open
902
+
903
+ async def __aenter__(self):
904
+ if not self._instance:
905
+ self._instance = await Browser.create(
906
+ self._config,
907
+ user_data_dir=self._user_data_dir,
908
+ headless=self._headless,
909
+ browser_executable_path=self._browser_executable_path,
910
+ browser_args=self._browser_args,
911
+ sandbox=self._sandbox,
912
+ host=self._host,
913
+ port=self._port,
914
+ **self._kwargs,
915
+ )
916
+ return self._instance
917
+
918
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
919
+ if not self._keep_open:
920
+ await util.deconstruct_browser(self._instance)
921
+
922
+
923
+ atexit.register(util.deconstruct_browser)