funbrowser 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.
Files changed (57) hide show
  1. funbrowser/__init__.py +120 -0
  2. funbrowser/_cdp.py +181 -0
  3. funbrowser/_errors.py +32 -0
  4. funbrowser/_flags.py +89 -0
  5. funbrowser/_launcher.py +153 -0
  6. funbrowser/browser.py +281 -0
  7. funbrowser/context.py +163 -0
  8. funbrowser/context_pool.py +162 -0
  9. funbrowser/element.py +258 -0
  10. funbrowser/fingerprint/__init__.py +14 -0
  11. funbrowser/fingerprint/data.py +74 -0
  12. funbrowser/fingerprint/presets.py +588 -0
  13. funbrowser/geo.py +139 -0
  14. funbrowser/humanly.py +188 -0
  15. funbrowser/panel.py +1181 -0
  16. funbrowser/pool.py +152 -0
  17. funbrowser/profile.py +73 -0
  18. funbrowser/proxy.py +236 -0
  19. funbrowser/py.typed +0 -0
  20. funbrowser/solver/__init__.py +12 -0
  21. funbrowser/solver/bridge.py +167 -0
  22. funbrowser/solver/client.py +244 -0
  23. funbrowser/solver/scripts/__init__.py +0 -0
  24. funbrowser/solver/scripts/_bootstrap.js +30 -0
  25. funbrowser/solver/scripts/funcaptcha.js +74 -0
  26. funbrowser/solver/scripts/geetest.js +76 -0
  27. funbrowser/solver/scripts/hcaptcha.js +76 -0
  28. funbrowser/solver/scripts/recaptcha_v2.js +79 -0
  29. funbrowser/solver/scripts/recaptcha_v3.js +45 -0
  30. funbrowser/solver/scripts/turnstile.js +60 -0
  31. funbrowser/stealth/__init__.py +13 -0
  32. funbrowser/stealth/flags.py +54 -0
  33. funbrowser/stealth/patches.py +214 -0
  34. funbrowser/stealth/scripts/__init__.py +0 -0
  35. funbrowser/stealth/scripts/_camouflage.js +32 -0
  36. funbrowser/stealth/scripts/_cleanup.js +8 -0
  37. funbrowser/stealth/scripts/audio_noise.js +32 -0
  38. funbrowser/stealth/scripts/canvas_noise.js +43 -0
  39. funbrowser/stealth/scripts/chrome_runtime.js +53 -0
  40. funbrowser/stealth/scripts/hardware.js +15 -0
  41. funbrowser/stealth/scripts/languages.js +13 -0
  42. funbrowser/stealth/scripts/permissions.js +15 -0
  43. funbrowser/stealth/scripts/platform.js +18 -0
  44. funbrowser/stealth/scripts/plugins.js +37 -0
  45. funbrowser/stealth/scripts/screen_props.js +18 -0
  46. funbrowser/stealth/scripts/webdriver.js +14 -0
  47. funbrowser/stealth/scripts/webgl.js +27 -0
  48. funbrowser/stealth/scripts/webrtc.js +45 -0
  49. funbrowser/tab.py +345 -0
  50. funbrowser/tls/__init__.py +25 -0
  51. funbrowser/tls/ca.py +181 -0
  52. funbrowser/tls/http.py +145 -0
  53. funbrowser/tls/mitm.py +326 -0
  54. funbrowser-0.1.0.dist-info/METADATA +316 -0
  55. funbrowser-0.1.0.dist-info/RECORD +57 -0
  56. funbrowser-0.1.0.dist-info/WHEEL +4 -0
  57. funbrowser-0.1.0.dist-info/licenses/LICENSE +21 -0
funbrowser/element.py ADDED
@@ -0,0 +1,258 @@
1
+ """ElementHandle — a chainable reference to a DOM element in a Tab.
2
+
3
+ Held by an opaque CDP ``objectId`` so the same JS object is targeted on each
4
+ call even if the surrounding DOM mutates. Becomes stale (and methods will
5
+ raise) after the page navigates away — get a fresh handle via ``tab.find`` or
6
+ ``tab.query`` after navigation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from . import humanly as _humanly
15
+
16
+ if TYPE_CHECKING:
17
+ from .tab import Tab
18
+
19
+
20
+ class ElementHandle:
21
+ __slots__ = ("_object_id", "_tab")
22
+
23
+ def __init__(self, tab: Tab, object_id: str) -> None:
24
+ self._tab = tab
25
+ self._object_id = object_id
26
+
27
+ @property
28
+ def object_id(self) -> str:
29
+ return self._object_id
30
+
31
+ async def _call(
32
+ self,
33
+ function_declaration: str,
34
+ args: list[Any] | None = None,
35
+ ) -> Any:
36
+ params: dict[str, Any] = {
37
+ "objectId": self._object_id,
38
+ "functionDeclaration": function_declaration,
39
+ "returnByValue": True,
40
+ "awaitPromise": True,
41
+ }
42
+ if args is not None:
43
+ params["arguments"] = [{"value": a} for a in args]
44
+ result = await self._tab._cdp.send(
45
+ "Runtime.callFunctionOn", params, session_id=self._tab.session_id
46
+ )
47
+ if "exceptionDetails" in result:
48
+ raise RuntimeError(f"JS exception: {result['exceptionDetails'].get('text', '')}")
49
+ return result.get("result", {}).get("value")
50
+
51
+ async def _call_objects(
52
+ self,
53
+ function_declaration: str,
54
+ args: list[Any] | None = None,
55
+ ) -> list[str]:
56
+ """Call a function that returns an array of elements; return their objectIds."""
57
+ params: dict[str, Any] = {
58
+ "objectId": self._object_id,
59
+ "functionDeclaration": function_declaration,
60
+ "returnByValue": False,
61
+ "awaitPromise": True,
62
+ }
63
+ if args is not None:
64
+ params["arguments"] = [{"value": a} for a in args]
65
+ result = await self._tab._cdp.send(
66
+ "Runtime.callFunctionOn", params, session_id=self._tab.session_id
67
+ )
68
+ if "exceptionDetails" in result:
69
+ raise RuntimeError(f"JS exception: {result['exceptionDetails'].get('text', '')}")
70
+ array_id = result.get("result", {}).get("objectId")
71
+ if not array_id:
72
+ return []
73
+ props = await self._tab._cdp.send(
74
+ "Runtime.getProperties",
75
+ {"objectId": array_id, "ownProperties": True},
76
+ session_id=self._tab.session_id,
77
+ )
78
+ ids: list[str] = []
79
+ for prop in props.get("result", []):
80
+ if not prop.get("enumerable"):
81
+ continue
82
+ value = prop.get("value", {})
83
+ obj_id = value.get("objectId")
84
+ if obj_id and value.get("subtype") == "node":
85
+ ids.append(obj_id)
86
+ return ids
87
+
88
+ # ── interaction ────────────────────────────────────────────────────
89
+
90
+ async def click(self) -> None:
91
+ """Click via real Input.dispatchMouseEvent at the element's centre.
92
+
93
+ With ``humanly`` active on the tab the cursor curves toward the
94
+ target (cubic Bezier + ease-in-out), the button-press holds for a
95
+ random duration, and the impact point includes pixel-level jitter
96
+ — none of which are visible at the DOM level but all of which
97
+ modern antibots score on. Without humanly, it's an instant click.
98
+
99
+ Briefly retries (up to ~1.5s) on a zero-size box — covers elements
100
+ that are visible-soon (CSS transitions, ``display:none`` toggles,
101
+ late layout passes) without the caller having to wait explicitly.
102
+ """
103
+ loop = asyncio.get_running_loop()
104
+ deadline = loop.time() + 1.5
105
+ box = None
106
+ while True:
107
+ box = await self._call(
108
+ "function() {"
109
+ " this.scrollIntoView({block:'center', inline:'center'});"
110
+ " const r = this.getBoundingClientRect();"
111
+ " return {x:r.left+r.width/2, y:r.top+r.height/2,"
112
+ " w:r.width, h:r.height};"
113
+ "}"
114
+ )
115
+ if box and box["w"] > 0 and box["h"] > 0:
116
+ break
117
+ if loop.time() >= deadline:
118
+ raise ValueError("element has zero size — cannot click")
119
+ await asyncio.sleep(0.05)
120
+ behaviour = self._tab._humanly
121
+ x, y = _humanly.jitter_target(behaviour, float(box["x"]), float(box["y"]))
122
+ await _humanly.move(self._tab, x, y)
123
+ await _humanly.pre_action_delay(behaviour)
124
+ await self._tab._send(
125
+ "Input.dispatchMouseEvent",
126
+ {
127
+ "type": "mousePressed",
128
+ "x": x,
129
+ "y": y,
130
+ "button": "left",
131
+ "clickCount": 1,
132
+ },
133
+ )
134
+ await _humanly.click_hold(behaviour)
135
+ await self._tab._send(
136
+ "Input.dispatchMouseEvent",
137
+ {
138
+ "type": "mouseReleased",
139
+ "x": x,
140
+ "y": y,
141
+ "button": "left",
142
+ "clickCount": 1,
143
+ },
144
+ )
145
+ self._tab._cursor = (x, y)
146
+
147
+ async def focus(self) -> None:
148
+ await self._call("function() { this.focus(); }")
149
+
150
+ async def type(self, text: str, *, delay_ms: float = 0.0) -> None:
151
+ """Focus the element and insert ``text``.
152
+
153
+ ``delay_ms > 0`` pauses that long between characters. With ``humanly``
154
+ active and ``delay_ms == 0`` (the default), the pause is randomised
155
+ per keystroke using the tab's behaviour profile, so keystroke timing
156
+ looks like a real user instead of an even cadence.
157
+
158
+ Uses ``Input.insertText`` rather than synthesised ``keyDown``/``keyUp``
159
+ events — more reliable across IME / layout cases. If a caller needs
160
+ actual key codes (shortcuts, Tab key), reach for raw CDP directly.
161
+ """
162
+ await self.focus()
163
+ behaviour = self._tab._humanly
164
+ if delay_ms > 0:
165
+ for ch in text:
166
+ await self._tab._send("Input.insertText", {"text": ch})
167
+ await asyncio.sleep(delay_ms / 1000.0)
168
+ elif behaviour is not None:
169
+ for ch in text:
170
+ await self._tab._send("Input.insertText", {"text": ch})
171
+ await asyncio.sleep(_humanly.type_delay(behaviour))
172
+ else:
173
+ await self._tab._send("Input.insertText", {"text": text})
174
+
175
+ async def fill(self, value: str) -> None:
176
+ """Set ``element.value`` and dispatch input + change events.
177
+
178
+ Faster than typing for forms where the page only cares about the
179
+ final value. Use ``type`` when per-keystroke handlers matter.
180
+ """
181
+ await self._call(
182
+ "function(v) {"
183
+ " this.focus();"
184
+ " this.value = v;"
185
+ " this.dispatchEvent(new Event('input', {bubbles:true}));"
186
+ " this.dispatchEvent(new Event('change', {bubbles:true}));"
187
+ "}",
188
+ [value],
189
+ )
190
+
191
+ async def hover(self) -> None:
192
+ box = await self._call(
193
+ "function() {"
194
+ " this.scrollIntoView({block:'center', inline:'center'});"
195
+ " const r = this.getBoundingClientRect();"
196
+ " return {x:r.left+r.width/2, y:r.top+r.height/2};"
197
+ "}"
198
+ )
199
+ if not box:
200
+ raise ValueError("element not visible")
201
+ behaviour = self._tab._humanly
202
+ x, y = _humanly.jitter_target(behaviour, float(box["x"]), float(box["y"]))
203
+ await _humanly.move(self._tab, x, y)
204
+
205
+ # ── reading ────────────────────────────────────────────────────────
206
+
207
+ async def text(self) -> str:
208
+ return (await self._call("function(){return this.innerText;}")) or ""
209
+
210
+ async def value(self) -> str:
211
+ return (await self._call("function(){return this.value;}")) or ""
212
+
213
+ async def attribute(self, name: str) -> str | None:
214
+ result = await self._call("function(n){return this.getAttribute(n);}", [name])
215
+ return None if result is None else str(result)
216
+
217
+ async def html(self, *, outer: bool = True) -> str:
218
+ if outer:
219
+ return (await self._call("function(){return this.outerHTML;}")) or ""
220
+ return (await self._call("function(){return this.innerHTML;}")) or ""
221
+
222
+ async def is_visible(self) -> bool:
223
+ return bool(
224
+ await self._call(
225
+ "function(){"
226
+ " const r = this.getBoundingClientRect();"
227
+ " const s = window.getComputedStyle(this);"
228
+ " return r.width>0 && r.height>0 && s.display!=='none' && s.visibility!=='hidden';"
229
+ "}"
230
+ )
231
+ )
232
+
233
+ async def bounding_box(self) -> dict[str, float] | None:
234
+ result = await self._call(
235
+ "function(){"
236
+ " const r = this.getBoundingClientRect();"
237
+ " return {x:r.left, y:r.top, width:r.width, height:r.height};"
238
+ "}"
239
+ )
240
+ if result is None:
241
+ return None
242
+ return {k: float(v) for k, v in result.items()}
243
+
244
+ # ── nested queries ─────────────────────────────────────────────────
245
+
246
+ async def query(self, selector: str) -> ElementHandle | None:
247
+ ids = await self._call_objects(
248
+ "function(s){ const r=this.querySelector(s); return r ? [r] : []; }",
249
+ [selector],
250
+ )
251
+ return ElementHandle(self._tab, ids[0]) if ids else None
252
+
253
+ async def query_all(self, selector: str) -> list[ElementHandle]:
254
+ ids = await self._call_objects(
255
+ "function(s){ return Array.from(this.querySelectorAll(s)); }",
256
+ [selector],
257
+ )
258
+ return [ElementHandle(self._tab, i) for i in ids]
@@ -0,0 +1,14 @@
1
+ """Fingerprint customization — swap the JS-visible identity the browser presents.
2
+
3
+ Use a preset (``presets.windows_11_nvidia_rtx_4070()``), build a custom
4
+ ``Fingerprint(...)``, or merge a preset with overrides via ``Fingerprint.merge``.
5
+
6
+ Pass to ``funbrowser.start(fingerprint=...)`` — see ``examples/custom_fingerprint.py``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from . import presets
12
+ from .data import Fingerprint
13
+
14
+ __all__ = ["Fingerprint", "presets"]
@@ -0,0 +1,74 @@
1
+ """The Fingerprint dataclass — every field optional; ``None`` means "don't override"."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from dataclasses import dataclass, field, fields
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Fingerprint:
12
+ """A snapshot of overridable browser-identity values.
13
+
14
+ Every field is optional. ``None`` means "leave whatever Chrome reports
15
+ natively". Use :func:`merge` to layer overrides on top of a preset.
16
+ """
17
+
18
+ # ── User-Agent + Client Hints ────────────────────────────────────────
19
+ user_agent: str | None = None
20
+ accept_language: str | None = None
21
+ platform: str | None = None # "Windows" / "macOS" / "Linux" / "Android"
22
+ platform_version: str | None = None # "15.0.0" etc. for Sec-CH-UA-Platform-Version
23
+ architecture: str | None = None # "x86" / "arm"
24
+ bitness: str | None = None # "64" / "32"
25
+ mobile: bool | None = None
26
+ brands: tuple[tuple[str, str], ...] | None = None # ((brand, version), ...)
27
+
28
+ # ── navigator.* ──────────────────────────────────────────────────────
29
+ languages: tuple[str, ...] | None = None
30
+ hardware_concurrency: int | None = None # logical CPU cores
31
+ device_memory: float | None = None # GB, Chrome reports {0.25,0.5,1,2,4,8}
32
+ max_touch_points: int | None = None
33
+
34
+ # ── screen / window ──────────────────────────────────────────────────
35
+ screen_width: int | None = None
36
+ screen_height: int | None = None
37
+ avail_width: int | None = None
38
+ avail_height: int | None = None
39
+ color_depth: int | None = None
40
+ device_pixel_ratio: float | None = None
41
+
42
+ # ── locale / time ────────────────────────────────────────────────────
43
+ timezone: str | None = None # IANA, e.g. "America/New_York"
44
+ locale: str | None = None # e.g. "en-US"
45
+
46
+ # ── WebGL ───────────────────────────────────────────────────────────
47
+ # Important: spoofing these without also faking the rendered pixel
48
+ # output (M9) can flag you with top-tier antibots. Default (None) keeps
49
+ # the real GPU value, which is usually what you want.
50
+ webgl_vendor: str | None = None
51
+ webgl_renderer: str | None = None
52
+
53
+ # ── meta ────────────────────────────────────────────────────────────
54
+ # Free-form label for debugging / logging. Never sent to the browser.
55
+ label: str = ""
56
+
57
+ # Tags useful for filtering presets programmatically.
58
+ tags: tuple[str, ...] = field(default_factory=tuple)
59
+
60
+ def merge(self, other: Fingerprint) -> Fingerprint:
61
+ """Return a new Fingerprint with ``other``'s non-None fields layered on."""
62
+ overrides: dict[str, Any] = {}
63
+ for f in fields(other):
64
+ value = getattr(other, f.name)
65
+ if f.name in ("label", "tags"):
66
+ if value:
67
+ overrides[f.name] = value
68
+ continue
69
+ if value is not None:
70
+ overrides[f.name] = value
71
+ return dataclasses.replace(self, **overrides)
72
+
73
+ def has_webgl_override(self) -> bool:
74
+ return self.webgl_vendor is not None or self.webgl_renderer is not None