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,564 @@
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 collections
11
+ import contextlib
12
+ import inspect
13
+ import itertools
14
+ import json
15
+ import logging
16
+ import re
17
+ import types
18
+ from asyncio import iscoroutine, iscoroutinefunction
19
+ from typing import Any, Awaitable, Callable, Generator, List, TypeVar, Union
20
+
21
+ import websockets.asyncio.client
22
+ from websockets import InvalidStatus
23
+
24
+ from .. import cdp
25
+ from . import browser as _browser
26
+ from . import util
27
+
28
+ T = TypeVar("T")
29
+
30
+ GLOBAL_DELAY = 0.005
31
+ MAX_SIZE: int = 2**28
32
+ PING_TIMEOUT: int = 900 # 15 minutes
33
+
34
+ TargetType = Union[cdp.target.TargetInfo, cdp.target.TargetID, str]
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class ProtocolException(Exception):
40
+ def __init__(self, *args, **kwargs): # real signature unknown
41
+
42
+ self.message = None
43
+ self.code = None
44
+ self.args = args
45
+ self.extra = kwargs
46
+
47
+ if isinstance(args[0], dict):
48
+
49
+ self.message = args[0].get("message", None) # noqa
50
+ self.code = args[0].get("code", None)
51
+
52
+ elif hasattr(args[0], "to_json"):
53
+
54
+ def serialize(obj, _d=0):
55
+ res = "\n"
56
+ for k, v in obj.items():
57
+ space = "\t" * _d
58
+ if isinstance(v, dict):
59
+ res += f"{space}{k}: {serialize(v, _d + 1)}\n"
60
+ else:
61
+ res += f"{space}{k}: {v}\n"
62
+
63
+ return res
64
+
65
+ self.message = serialize(args[0].to_json())
66
+
67
+ else:
68
+ self.message = "| ".join(str(x) for x in args)
69
+
70
+ self.message += "\n".join(f"{k}:{v}" for k, v in self.extra.items())
71
+
72
+ def __str__(self):
73
+ return (
74
+ f"{self.message} [code: {self.code}]"
75
+ if self.code
76
+ else f"{self.message}" + f"\n{self.extra}"
77
+ )
78
+
79
+
80
+ class Transaction:
81
+ def __init__(self, request: dict):
82
+ self.id: int | None = request["id"]
83
+ self.request: dict = request
84
+ self.events: list = []
85
+ self.result: Any = None
86
+
87
+ def __str__(self):
88
+ return (
89
+ f"{self.__class__.__name__}"
90
+ f"[\n"
91
+ f"request: {self._truncate(self.request)}\n"
92
+ f"events:{len(self.events)}\n"
93
+ f"result: {self._truncate(self.result)}\n"
94
+ f"]"
95
+ )
96
+
97
+ def __repr__(self):
98
+ return self.__str__()
99
+
100
+ @staticmethod
101
+ def _truncate(item: Any, max_length: int = 256, ellipsis: bool = True) -> str:
102
+ string = str(item)
103
+ if len(string) > max_length:
104
+ return f"{string[:max_length]} {'...' if ellipsis else ''}"
105
+ return string
106
+
107
+ # class MapperEntry(asyncio.Future):
108
+
109
+
110
+ #
111
+ # def __init__(self, cdp_obj: Generator, id: int, session_id: str = None):
112
+ #
113
+ # super().__init__()
114
+ # self._cdp_obj = cdp_obj
115
+ #
116
+ # method, *params = next(self._cdp_obj).values()
117
+ # if params:
118
+ # params = params.pop()
119
+ #
120
+ # request = {"method": method, "params": params, "id": id}
121
+ # if session_id:
122
+ # request["sessionId"] = session_id
123
+ #
124
+ # self.request = request
125
+ # self.response = None
126
+ # self.events = []
127
+ #
128
+ # def set_session_id(self, session_id: str):
129
+ # self.request['sessionId'] = session_id
130
+ #
131
+ # def __call__(self, response: dict):
132
+ # self.response = response
133
+ # if "error" in response:
134
+ # # set exception and bail out
135
+ # return self.set_exception(ProtocolException(response["error"], request=self.request))
136
+ # try:
137
+ # # try to parse the result according to the py cdp docs.
138
+ # self._cdp_obj.send(response["result"])
139
+ # except StopIteration as e:
140
+ # # exception value holds the parsed response
141
+ # self.set_result(e.value)
142
+
143
+
144
+ class Connection:
145
+ socket: websockets.asyncio.client.ClientConnection | None
146
+ websocket_url: str = None
147
+
148
+ @property
149
+ def attached(self):
150
+ return bool(self.session_id and self.target)
151
+
152
+ @property
153
+ def transactions(self):
154
+ """
155
+ simple array that holds recent transactions.
156
+ each transaction consist of a response, request, result, events fields
157
+ the latest transaction is the last element in the list.
158
+ """
159
+ return self._transactions
160
+
161
+ @property
162
+ def last_transaction(self):
163
+ """
164
+ simple alias to get the last transaction to inspect.
165
+ """
166
+ return self.transactions[-1]
167
+
168
+ def add_handler(self, event_type: type, callback: Callable) -> None:
169
+ """Register an event callback for a parsed CDP event type."""
170
+ if callback not in self.handlers[event_type]:
171
+ self.handlers[event_type].append(callback)
172
+
173
+ def remove_handler(self, event_type: type, callback: Callable) -> bool:
174
+ """Remove an event callback; returns True when a handler was removed."""
175
+ callbacks = self.handlers.get(event_type)
176
+ if not callbacks:
177
+ return False
178
+ try:
179
+ callbacks.remove(callback)
180
+ except ValueError:
181
+ return False
182
+ if not callbacks:
183
+ self.handlers.pop(event_type, None)
184
+ return True
185
+
186
+ def __init__(
187
+ self,
188
+ target: TargetType = None,
189
+ parent: Connection = None,
190
+ auto_attach: bool = False,
191
+ **kwargs,
192
+ ) -> None:
193
+
194
+ self.session_id = None
195
+ self.websocket_url = parent.websocket_url if parent else None
196
+ self.handlers = collections.defaultdict(list)
197
+ self.lock = asyncio.Lock()
198
+ self.socket = None
199
+ self.target = target
200
+ self.parent = parent
201
+ self.__count__ = itertools.count(0)
202
+ self._auto_attach = auto_attach
203
+ self._transactions: List[Transaction] = []
204
+ self._targets: List[Connection] = []
205
+ self._mapper: dict[int, asyncio.Future] = {}
206
+ self._tx_by_id: dict[int, Transaction] = {}
207
+ self._listener_task: asyncio.Task | None = None
208
+
209
+ #
210
+ # @classmethod
211
+ # async def from_parent(
212
+ # cls,
213
+ # target: cdp.target.TargetID | cdp.target.TargetInfo,
214
+ # parent: Connection,
215
+ # auto_attach: bool = False,
216
+ # ) -> Connection:
217
+ # target_id = None
218
+ # if isinstance(target, cdp.target.TargetInfo):
219
+ # target_id = target.target_id
220
+ # elif isinstance(target, (cdp.target.TargetID, str)):
221
+ # target_id = target
222
+ # instance = cls(target=target_id, parent=parent, auto_attach=auto_attach)
223
+ # await instance.attach()
224
+ # return instance
225
+
226
+ async def aopen(self):
227
+ """ """
228
+ if not self.websocket_url:
229
+ raise RuntimeError("having no parent and no websocket url")
230
+
231
+ if not self.socket or bool(self.socket.close_code):
232
+ self.socket = await websockets.connect(
233
+ self.websocket_url,
234
+ ping_timeout=PING_TIMEOUT,
235
+ max_size=MAX_SIZE,
236
+ )
237
+ self._listener_task = asyncio.create_task(self._listener())
238
+
239
+ async def aclose(self):
240
+ """ """
241
+ self._fail_pending_futures(ConnectionError("Connection closing"))
242
+ if self._listener_task is not None:
243
+ self._listener_task.cancel()
244
+ with contextlib.suppress(asyncio.CancelledError):
245
+ await self._listener_task
246
+ self._listener_task = None
247
+ if self.socket:
248
+ await self.socket.close()
249
+ await self.socket.wait_closed()
250
+ self.socket = None
251
+
252
+ def _fail_pending_futures(self, exc: BaseException) -> None:
253
+ for future in self._mapper.values():
254
+ if not future.done():
255
+ if isinstance(exc, asyncio.CancelledError):
256
+ future.cancel()
257
+ else:
258
+ future.set_exception(exc)
259
+ self._mapper.clear()
260
+
261
+ async def attach(self, target: TargetType = None):
262
+ """ " """
263
+ await self.aopen()
264
+ target = target or self.target
265
+
266
+ if not target:
267
+ # is browser target
268
+
269
+ self.handlers[cdp.target.AttachedToTarget] = [
270
+ lambda event: setattr(self, "target", event.target_info)
271
+ ]
272
+ self.session_id = await self.send(
273
+ cdp.target.attach_to_browser_target(), _attach=True
274
+ )
275
+ self.handlers[cdp.target.AttachedToTarget] = []
276
+ self.handlers.clear()
277
+ if self._auto_attach:
278
+ self.handlers[cdp.target.AttachedToTarget].append(self._attach_handler)
279
+ self.handlers[cdp.target.DetachedFromTarget].append(
280
+ self._attach_handler
281
+ )
282
+ await self.send(
283
+ cdp.target.auto_attach_related(self.target.target_id, False)
284
+ )
285
+
286
+ else:
287
+ target_id = None
288
+ if isinstance(target, cdp.target.TargetInfo):
289
+ target_id = target.target_id
290
+ elif isinstance(target, (cdp.target.TargetID, str)):
291
+ target_id = target
292
+ self.session_id = await self.send(
293
+ cdp.target.attach_to_target(target_id=target_id, flatten=True),
294
+ _attach=True,
295
+ )
296
+
297
+ self.target = await self.send(cdp.target.get_target_info(target_id))
298
+ if self._auto_attach:
299
+ self.handlers.clear()
300
+ self.handlers[cdp.target.AttachedToTarget].append(self._attach_handler)
301
+ self.handlers[cdp.target.DetachedFromTarget].append(
302
+ self._attach_handler
303
+ )
304
+ self.handlers[cdp.target.TargetInfoChanged].append(self._attach_handler)
305
+ self.handlers[cdp.target.TargetCrashed].append(self._attach_handler)
306
+ await self.send(
307
+ cdp.target.set_auto_attach(
308
+ True, wait_for_debugger_on_start=False, flatten=True
309
+ )
310
+ )
311
+ # self.target = await self.send(cdp.target.get_target_info(target_id))
312
+
313
+ async def detach(self):
314
+
315
+ for child in self._targets:
316
+ await child.detach()
317
+ await self.send(
318
+ cdp.target.detach_from_target(
319
+ session_id=self.session_id, target_id=self.target.target_id
320
+ ),
321
+ _attach=True,
322
+ )
323
+ self.session_id = None
324
+
325
+ self.handlers.clear()
326
+ self._targets.clear()
327
+
328
+ async def _attach_handler(
329
+ self,
330
+ event: (
331
+ cdp.target.AttachedToTarget
332
+ | cdp.target.DetachedFromTarget
333
+ | cdp.target.TargetInfoChanged
334
+ | cdp.target.TargetDestroyed
335
+ ),
336
+ ):
337
+ if isinstance(event, cdp.target.AttachedToTarget):
338
+ if event.target_info.target_id == self.target.target_id:
339
+ self.session_id = event.session_id
340
+ self.target = event.target_info
341
+ else:
342
+ if event.target_info.type_ == "browser":
343
+ return
344
+ elif event.target_info.type_ == "page":
345
+ from mithwire.core.tab import Tab
346
+
347
+ new_child = Tab(target=event.target_info, parent=self)
348
+ self._targets.append(new_child)
349
+ elif event.target_info.type_ == "iframe":
350
+ from mithwire.core.tab import IFrame
351
+
352
+ new_child = IFrame(target=event.target_info, parent=self)
353
+ self._targets.append(new_child)
354
+ else:
355
+ new_child = Connection(target=event.target_info, parent=self)
356
+ self._targets.append(new_child)
357
+
358
+ elif isinstance(event, cdp.target.DetachedFromTarget):
359
+ if event.target_id == self.target.target_id:
360
+ self.session_id = None
361
+ removed = [
362
+ child for child in self._targets if child.session_id == event.session_id
363
+ ]
364
+ for child in removed:
365
+ self._targets.remove(child)
366
+
367
+ elif isinstance(event, cdp.target.TargetInfoChanged):
368
+ if event.target_info.target_id == self.target.target_id:
369
+ self.target = event.target_info
370
+ else:
371
+ for child in self._targets:
372
+ if (
373
+ child.target
374
+ and child.target.target_id == event.target_info.target_id
375
+ ):
376
+ child.target = event.target_info
377
+
378
+ elif isinstance(event, cdp.target.TargetDestroyed):
379
+ if event.target_id == self.target.target_id:
380
+ self.target = None
381
+ else:
382
+ for child in self._targets.copy():
383
+ if child.target.target_id == event.target_id:
384
+ self._targets.remove(child)
385
+
386
+ async def send(
387
+ self,
388
+ cdp_obj: Generator[dict[str, Any], dict[str, Any], Any],
389
+ _attach: bool = False,
390
+ **kwargs,
391
+ ) -> Any:
392
+
393
+ # if self.parent and self.session_id:
394
+ # return await self.parent.send(cdp_obj, **kwargs)
395
+
396
+ if not _attach:
397
+ if not self.attached or not self.socket:
398
+ await self.attach()
399
+
400
+ method, *params = next(cdp_obj).values()
401
+ if params:
402
+ params = params.pop()
403
+ _id = next(self.__count__)
404
+ message = {"method": method, "params": params, "id": _id}
405
+ if not _attach:
406
+ message["sessionId"] = self.session_id
407
+ message.update(kwargs)
408
+
409
+ tx = Transaction(message)
410
+ self.transactions.append(tx)
411
+ self._tx_by_id[_id] = tx
412
+
413
+ future = asyncio.get_running_loop().create_future()
414
+ self._mapper[_id] = future
415
+
416
+ while len(self.transactions) > 25:
417
+ # clears the oldest request/response pair from transactions
418
+ removed_tx = self.transactions.pop(0)
419
+ if removed_tx.id is not None:
420
+ self._tx_by_id.pop(removed_tx.id, None)
421
+
422
+ ws = self.socket
423
+ if ws is None:
424
+ raise RuntimeError("WebSocket is not connected")
425
+
426
+ async with self.lock:
427
+ try:
428
+ await ws.send(json.dumps(message))
429
+ except Exception:
430
+ self._mapper.pop(_id, None)
431
+ if not future.done():
432
+ future.cancel()
433
+ raise
434
+
435
+ try:
436
+ response_message = await future
437
+ finally:
438
+ self._mapper.pop(_id, None)
439
+
440
+ if "error" in response_message:
441
+ exception = ProtocolException(response_message["error"])
442
+ tx.result = exception
443
+ raise exception
444
+
445
+ try:
446
+ cdp_obj.send(response_message["result"])
447
+ except StopIteration as e:
448
+ tx.result = e.value
449
+ return e.value
450
+ except (Exception,) as e:
451
+ tx.result = e
452
+ raise
453
+
454
+ async def _listener(self):
455
+ ws = self.socket
456
+ if ws is None:
457
+ raise RuntimeError("Listener started without an active socket connection.")
458
+
459
+ while True:
460
+ try:
461
+ raw = await ws.recv()
462
+ if not raw:
463
+ continue
464
+ message = json.loads(raw)
465
+
466
+ if "id" in message:
467
+ future = self._mapper.pop(message["id"], None)
468
+ if future and not future.done():
469
+ future.set_result(message)
470
+ elif future and future.done():
471
+ logger.warning(
472
+ "received response for already-completed future id=%s",
473
+ message.get("id"),
474
+ )
475
+ elif "method" in message:
476
+ # Unsolicited events are out-of-band unless an explicit tx id is provided.
477
+ await self.process_event(message, None)
478
+
479
+ except websockets.exceptions.ConnectionClosed:
480
+ self._fail_pending_futures(ConnectionError("Connection closed"))
481
+ break
482
+ except asyncio.CancelledError as e:
483
+ self._fail_pending_futures(e)
484
+ break
485
+ except Exception as e:
486
+ logger.error(f"background listener error: {e}", exc_info=True)
487
+
488
+ async def process_event(self, message: dict, tx_id: int | None = None) -> None:
489
+ """ """
490
+ event = None
491
+
492
+ try:
493
+ event = cdp.util.parse_json_event(message)
494
+ tx = self._tx_by_id.get(tx_id) if tx_id is not None else None
495
+ if tx_id is not None and tx is None:
496
+ logger.debug(
497
+ "received event for unknown transaction id=%s method=%s",
498
+ tx_id,
499
+ message.get("method"),
500
+ )
501
+ if tx is not None:
502
+ tx.events.append(event)
503
+ except KeyError as e:
504
+ logger.exception(e)
505
+ return
506
+ if type(event) in self.handlers:
507
+ callbacks = self.handlers[type(event)]
508
+ else:
509
+ return
510
+ if not callbacks:
511
+ return
512
+ for callback in callbacks:
513
+ try:
514
+ if iscoroutinefunction(callback) or iscoroutine(callback):
515
+ try:
516
+ # handler is defined with the second param being the tab instance
517
+ asyncio.create_task(callback(event, self))
518
+ except TypeError as e:
519
+ # handler is defined without the tab parameter
520
+ asyncio.create_task(callback(event))
521
+ # if this causes an exception it is handled by the outer handler
522
+ else:
523
+ try:
524
+ # handler is defined with the second param being the tab instance
525
+ callback(event, self)
526
+ except TypeError:
527
+ # handler is defined without the tab parameter
528
+ callback(event)
529
+ # if this causes an exception it is handled by the outer handler
530
+
531
+ except Exception as e:
532
+ logger.warning(
533
+ "exception in callback %s for event %s => %s",
534
+ callback,
535
+ event.__class__.__name__,
536
+ e,
537
+ exc_info=True,
538
+ )
539
+
540
+ def __str__(self):
541
+ name = self.__class__.__name__
542
+ if self.target and self.target.type_:
543
+ name = self.target.type_.title()
544
+ name = util.to_camel(name)
545
+ return (
546
+ f"<{name}\n"
547
+ f"\turl: {self.target.url if type(self.target) is cdp.target.TargetInfo else ''}\n"
548
+ f"\tattached: {self.attached}>\n"
549
+ )
550
+
551
+ #
552
+ # def __hash__(self):
553
+ # return hash(frozenset([self.target.target_id, self.session_id]))
554
+
555
+ # def __eq__(self, other: Union[Connection]):
556
+ # try:
557
+ # assert self.session_id and other.session_id and other.session_id == self.session_id
558
+ # assert self.target and other.target and self.target.target_id == other.target.target_id
559
+ # return True
560
+ # except (Exception,):
561
+ # return NotImplemented
562
+
563
+ def __repr__(self):
564
+ return self.__str__()