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/geo.py ADDED
@@ -0,0 +1,139 @@
1
+ """Infer timezone / locale / accept-language from a proxy's exit IP.
2
+
3
+ When a Browser is started with a proxy and ``geo_autoconfigure=True``
4
+ (the default), we route a short ``ip-api.com`` lookup through the proxy
5
+ to get the exit IP's country and timezone, then fill any
6
+ :class:`Fingerprint` fields the caller didn't set explicitly.
7
+
8
+ Calling site keeps full control: explicit ``timezone`` / ``locale`` on
9
+ the caller-supplied Fingerprint always win over the geo guess.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass
16
+
17
+ import httpx
18
+
19
+ from .proxy import Proxy
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Country code → primary BCP-47 locale. Covers the cases proxy providers
24
+ # routinely hand out; unknown countries fall back to country_code.lower() +
25
+ # '-' + country_code.upper() in resolve_locale().
26
+ _LOCALE: dict[str, str] = {
27
+ "US": "en-US",
28
+ "CA": "en-CA",
29
+ "GB": "en-GB",
30
+ "AU": "en-AU",
31
+ "IE": "en-IE",
32
+ "NZ": "en-NZ",
33
+ "DE": "de-DE",
34
+ "AT": "de-AT",
35
+ "CH": "de-CH",
36
+ "FR": "fr-FR",
37
+ "BE": "fr-BE",
38
+ "IT": "it-IT",
39
+ "ES": "es-ES",
40
+ "MX": "es-MX",
41
+ "AR": "es-AR",
42
+ "BR": "pt-BR",
43
+ "PT": "pt-PT",
44
+ "NL": "nl-NL",
45
+ "SE": "sv-SE",
46
+ "NO": "nb-NO",
47
+ "DK": "da-DK",
48
+ "FI": "fi-FI",
49
+ "PL": "pl-PL",
50
+ "CZ": "cs-CZ",
51
+ "RU": "ru-RU",
52
+ "UA": "uk-UA",
53
+ "KZ": "ru-KZ",
54
+ "BY": "be-BY",
55
+ "TR": "tr-TR",
56
+ "JP": "ja-JP",
57
+ "KR": "ko-KR",
58
+ "CN": "zh-CN",
59
+ "TW": "zh-TW",
60
+ "HK": "zh-HK",
61
+ "IN": "en-IN",
62
+ "ID": "id-ID",
63
+ "TH": "th-TH",
64
+ "VN": "vi-VN",
65
+ "IL": "he-IL",
66
+ "AE": "ar-AE",
67
+ "SA": "ar-SA",
68
+ "EG": "ar-EG",
69
+ }
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class GeoInfo:
74
+ """Geolocation hints derived from a proxy's exit IP."""
75
+
76
+ ip: str
77
+ country_code: str
78
+ country: str
79
+ region: str
80
+ city: str
81
+ timezone: str
82
+ locale: str
83
+ accept_language: str
84
+
85
+
86
+ def resolve_locale(country_code: str) -> str:
87
+ cc = country_code.upper()
88
+ if cc in _LOCALE:
89
+ return _LOCALE[cc]
90
+ return f"{cc.lower()}-{cc}"
91
+
92
+
93
+ def make_accept_language(locale: str) -> str:
94
+ """Build an Accept-Language header value: the locale + its base + 'en'."""
95
+ base = locale.split("-", 1)[0]
96
+ parts = [locale]
97
+ if base != locale:
98
+ parts.append(f"{base};q=0.9")
99
+ if base != "en":
100
+ parts.append("en;q=0.8")
101
+ return ",".join(parts)
102
+
103
+
104
+ async def lookup_proxy_geo(proxy: Proxy, *, timeout: float = 5.0) -> GeoInfo | None:
105
+ """Fetch geo information for the proxy's exit IP via ip-api.com.
106
+
107
+ Returns ``None`` on any failure (proxy down, ip-api rate-limited, etc.).
108
+ Never raises — the call site treats a missing result as "skip".
109
+ """
110
+ try:
111
+ proxy_url = proxy.url()
112
+ async with httpx.AsyncClient(proxy=proxy_url, timeout=timeout) as client:
113
+ r = await client.get(
114
+ "http://ip-api.com/json/",
115
+ params={"fields": "status,country,countryCode,region,city,timezone,query"},
116
+ )
117
+ r.raise_for_status()
118
+ data = r.json()
119
+ except Exception:
120
+ logger.debug("geo lookup failed", exc_info=True)
121
+ return None
122
+
123
+ if data.get("status") != "success":
124
+ return None
125
+
126
+ cc = str(data.get("countryCode") or "")
127
+ if not cc:
128
+ return None
129
+ locale = resolve_locale(cc)
130
+ return GeoInfo(
131
+ ip=str(data.get("query") or ""),
132
+ country_code=cc,
133
+ country=str(data.get("country") or ""),
134
+ region=str(data.get("region") or ""),
135
+ city=str(data.get("city") or ""),
136
+ timezone=str(data.get("timezone") or ""),
137
+ locale=locale,
138
+ accept_language=make_accept_language(locale),
139
+ )
funbrowser/humanly.py ADDED
@@ -0,0 +1,188 @@
1
+ """Human-like input timing & motion.
2
+
3
+ When ``humanly=True`` (or a custom :class:`HumanBehavior` is passed),
4
+ mouse moves trace a randomised cubic-Bezier curve with ease-in-out timing
5
+ instead of teleporting, click ``mousePressed`` / ``mouseReleased`` pairs
6
+ hold for a random duration, typing dispatches each character with a random
7
+ delay, and targets are hit with a few pixels of jitter from the centre.
8
+
9
+ These are the timing and trajectory signals modern antibots score on top
10
+ of the JS-level fingerprint patches in :mod:`funbrowser.stealth`. The
11
+ defaults are tuned to read like a moderately-paced human user;
12
+ :data:`FAST` and :data:`CAREFUL` cover the obvious ends.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import math
19
+ import random
20
+ from dataclasses import dataclass
21
+ from typing import TYPE_CHECKING
22
+
23
+ if TYPE_CHECKING:
24
+ from .tab import Tab
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class HumanBehavior:
29
+ # ── mouse motion ────────────────────────────────────────────────────
30
+ move_steps_min: int = 18
31
+ move_steps_max: int = 40
32
+ move_duration_ms_min: float = 220.0
33
+ move_duration_ms_max: float = 650.0
34
+ curve_strength_px: float = 60.0
35
+ jitter_px: float = 1.5
36
+
37
+ # ── click ──────────────────────────────────────────────────────────
38
+ pre_click_delay_ms_min: float = 60.0
39
+ pre_click_delay_ms_max: float = 260.0
40
+ click_hold_ms_min: float = 45.0
41
+ click_hold_ms_max: float = 130.0
42
+ target_jitter_px: float = 3.0
43
+
44
+ # ── typing ─────────────────────────────────────────────────────────
45
+ type_delay_ms_min: float = 65.0
46
+ type_delay_ms_max: float = 195.0
47
+
48
+
49
+ DEFAULT = HumanBehavior()
50
+
51
+ FAST = HumanBehavior(
52
+ move_steps_min=8,
53
+ move_steps_max=16,
54
+ move_duration_ms_min=80.0,
55
+ move_duration_ms_max=220.0,
56
+ curve_strength_px=30.0,
57
+ pre_click_delay_ms_min=15.0,
58
+ pre_click_delay_ms_max=80.0,
59
+ click_hold_ms_min=20.0,
60
+ click_hold_ms_max=60.0,
61
+ type_delay_ms_min=25.0,
62
+ type_delay_ms_max=85.0,
63
+ )
64
+
65
+ CAREFUL = HumanBehavior(
66
+ move_steps_min=30,
67
+ move_steps_max=70,
68
+ move_duration_ms_min=450.0,
69
+ move_duration_ms_max=1300.0,
70
+ curve_strength_px=90.0,
71
+ pre_click_delay_ms_min=220.0,
72
+ pre_click_delay_ms_max=900.0,
73
+ click_hold_ms_min=85.0,
74
+ click_hold_ms_max=210.0,
75
+ type_delay_ms_min=140.0,
76
+ type_delay_ms_max=420.0,
77
+ )
78
+
79
+
80
+ def _ease_in_out(t: float) -> float:
81
+ """Smoothstep — slow at endpoints, fast in the middle."""
82
+ return 3 * t * t - 2 * t * t * t
83
+
84
+
85
+ def _cubic_bezier(
86
+ t: float,
87
+ p0: tuple[float, float],
88
+ p1: tuple[float, float],
89
+ p2: tuple[float, float],
90
+ p3: tuple[float, float],
91
+ ) -> tuple[float, float]:
92
+ u = 1.0 - t
93
+ x = u**3 * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t**3 * p3[0]
94
+ y = u**3 * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t**3 * p3[1]
95
+ return x, y
96
+
97
+
98
+ async def move(tab: Tab, target_x: float, target_y: float) -> None:
99
+ """Move the virtual cursor to ``(target_x, target_y)`` along a curve.
100
+
101
+ Falls back to a single ``mouseMoved`` event when no humanly profile is
102
+ active on the tab.
103
+ """
104
+ behaviour: HumanBehavior | None = tab._humanly
105
+ if behaviour is None:
106
+ await tab._send(
107
+ "Input.dispatchMouseEvent",
108
+ {"type": "mouseMoved", "x": target_x, "y": target_y},
109
+ )
110
+ tab._cursor = (target_x, target_y)
111
+ return
112
+
113
+ start = tab._cursor or (target_x, target_y)
114
+ if start == (target_x, target_y):
115
+ # Nothing to do for the first interaction with no recorded cursor.
116
+ await tab._send(
117
+ "Input.dispatchMouseEvent",
118
+ {"type": "mouseMoved", "x": target_x, "y": target_y},
119
+ )
120
+ tab._cursor = (target_x, target_y)
121
+ return
122
+
123
+ dx = target_x - start[0]
124
+ dy = target_y - start[1]
125
+ curve = behaviour.curve_strength_px
126
+
127
+ cp1 = (
128
+ start[0] + dx * 0.3 + random.uniform(-curve, curve),
129
+ start[1] + dy * 0.3 + random.uniform(-curve, curve),
130
+ )
131
+ cp2 = (
132
+ start[0] + dx * 0.7 + random.uniform(-curve, curve),
133
+ start[1] + dy * 0.7 + random.uniform(-curve, curve),
134
+ )
135
+
136
+ n_steps = random.randint(behaviour.move_steps_min, behaviour.move_steps_max)
137
+ duration_s = (
138
+ random.uniform(behaviour.move_duration_ms_min, behaviour.move_duration_ms_max) / 1000.0
139
+ )
140
+ per_step = duration_s / max(n_steps, 1)
141
+ jitter = behaviour.jitter_px
142
+
143
+ for i in range(1, n_steps + 1):
144
+ eased = _ease_in_out(i / n_steps)
145
+ x, y = _cubic_bezier(eased, start, cp1, cp2, (target_x, target_y))
146
+ x += random.uniform(-jitter, jitter)
147
+ y += random.uniform(-jitter, jitter)
148
+ await tab._send(
149
+ "Input.dispatchMouseEvent",
150
+ {"type": "mouseMoved", "x": x, "y": y},
151
+ )
152
+ await asyncio.sleep(per_step)
153
+
154
+ tab._cursor = (target_x, target_y)
155
+
156
+
157
+ def jitter_target(behaviour: HumanBehavior | None, x: float, y: float) -> tuple[float, float]:
158
+ if behaviour is None:
159
+ return x, y
160
+ r = behaviour.target_jitter_px
161
+ if r <= 0:
162
+ return x, y
163
+ return x + random.uniform(-r, r), y + random.uniform(-r, r)
164
+
165
+
166
+ async def pre_action_delay(behaviour: HumanBehavior | None) -> None:
167
+ if behaviour is None:
168
+ return
169
+ delay = random.uniform(behaviour.pre_click_delay_ms_min, behaviour.pre_click_delay_ms_max)
170
+ await asyncio.sleep(delay / 1000.0)
171
+
172
+
173
+ async def click_hold(behaviour: HumanBehavior | None) -> None:
174
+ if behaviour is None:
175
+ return
176
+ delay = random.uniform(behaviour.click_hold_ms_min, behaviour.click_hold_ms_max)
177
+ await asyncio.sleep(delay / 1000.0)
178
+
179
+
180
+ def type_delay(behaviour: HumanBehavior | None) -> float:
181
+ if behaviour is None:
182
+ return 0.0
183
+ return random.uniform(behaviour.type_delay_ms_min, behaviour.type_delay_ms_max) / 1000.0
184
+
185
+
186
+ # Kept around in case future code wants to compute distance-aware step counts.
187
+ def _euclid(p: tuple[float, float], q: tuple[float, float]) -> float:
188
+ return math.hypot(p[0] - q[0], p[1] - q[1])