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.
- funbrowser/__init__.py +120 -0
- funbrowser/_cdp.py +181 -0
- funbrowser/_errors.py +32 -0
- funbrowser/_flags.py +89 -0
- funbrowser/_launcher.py +153 -0
- funbrowser/browser.py +281 -0
- funbrowser/context.py +163 -0
- funbrowser/context_pool.py +162 -0
- funbrowser/element.py +258 -0
- funbrowser/fingerprint/__init__.py +14 -0
- funbrowser/fingerprint/data.py +74 -0
- funbrowser/fingerprint/presets.py +588 -0
- funbrowser/geo.py +139 -0
- funbrowser/humanly.py +188 -0
- funbrowser/panel.py +1181 -0
- funbrowser/pool.py +152 -0
- funbrowser/profile.py +73 -0
- funbrowser/proxy.py +236 -0
- funbrowser/py.typed +0 -0
- funbrowser/solver/__init__.py +12 -0
- funbrowser/solver/bridge.py +167 -0
- funbrowser/solver/client.py +244 -0
- funbrowser/solver/scripts/__init__.py +0 -0
- funbrowser/solver/scripts/_bootstrap.js +30 -0
- funbrowser/solver/scripts/funcaptcha.js +74 -0
- funbrowser/solver/scripts/geetest.js +76 -0
- funbrowser/solver/scripts/hcaptcha.js +76 -0
- funbrowser/solver/scripts/recaptcha_v2.js +79 -0
- funbrowser/solver/scripts/recaptcha_v3.js +45 -0
- funbrowser/solver/scripts/turnstile.js +60 -0
- funbrowser/stealth/__init__.py +13 -0
- funbrowser/stealth/flags.py +54 -0
- funbrowser/stealth/patches.py +214 -0
- funbrowser/stealth/scripts/__init__.py +0 -0
- funbrowser/stealth/scripts/_camouflage.js +32 -0
- funbrowser/stealth/scripts/_cleanup.js +8 -0
- funbrowser/stealth/scripts/audio_noise.js +32 -0
- funbrowser/stealth/scripts/canvas_noise.js +43 -0
- funbrowser/stealth/scripts/chrome_runtime.js +53 -0
- funbrowser/stealth/scripts/hardware.js +15 -0
- funbrowser/stealth/scripts/languages.js +13 -0
- funbrowser/stealth/scripts/permissions.js +15 -0
- funbrowser/stealth/scripts/platform.js +18 -0
- funbrowser/stealth/scripts/plugins.js +37 -0
- funbrowser/stealth/scripts/screen_props.js +18 -0
- funbrowser/stealth/scripts/webdriver.js +14 -0
- funbrowser/stealth/scripts/webgl.js +27 -0
- funbrowser/stealth/scripts/webrtc.js +45 -0
- funbrowser/tab.py +345 -0
- funbrowser/tls/__init__.py +25 -0
- funbrowser/tls/ca.py +181 -0
- funbrowser/tls/http.py +145 -0
- funbrowser/tls/mitm.py +326 -0
- funbrowser-0.1.0.dist-info/METADATA +316 -0
- funbrowser-0.1.0.dist-info/RECORD +57 -0
- funbrowser-0.1.0.dist-info/WHEEL +4 -0
- 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])
|