nodriver-proxyhat 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.
- nodriver_proxyhat/__init__.py +15 -0
- nodriver_proxyhat/_auth.py +55 -0
- nodriver_proxyhat/_resolve.py +71 -0
- nodriver_proxyhat/proxy.py +162 -0
- nodriver_proxyhat-0.1.0.dist-info/METADATA +150 -0
- nodriver_proxyhat-0.1.0.dist-info/RECORD +8 -0
- nodriver_proxyhat-0.1.0.dist-info/WHEEL +4 -0
- nodriver_proxyhat-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""nodriver-proxyhat — route nodriver (undetected Chrome) through ProxyHat residential proxies."""
|
|
2
|
+
|
|
3
|
+
from nodriver_proxyhat._auth import enable_proxy_auth
|
|
4
|
+
from nodriver_proxyhat._resolve import ProxyHatConfigError, resolve_credentials
|
|
5
|
+
from nodriver_proxyhat.proxy import proxyhat_auth, proxyhat_browser, proxyhat_browser_args
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ProxyHatConfigError",
|
|
9
|
+
"enable_proxy_auth",
|
|
10
|
+
"proxyhat_auth",
|
|
11
|
+
"proxyhat_browser",
|
|
12
|
+
"proxyhat_browser_args",
|
|
13
|
+
"resolve_credentials",
|
|
14
|
+
]
|
|
15
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Wire ProxyHat gateway authentication onto a nodriver tab over raw CDP.
|
|
2
|
+
|
|
3
|
+
Chrome's ``--proxy-server`` flag can't carry a username/password, so a credentialed
|
|
4
|
+
residential gateway needs another way to answer the proxy's auth challenge. nodriver
|
|
5
|
+
speaks the Chrome DevTools Protocol directly, so we authenticate the CDP way:
|
|
6
|
+
|
|
7
|
+
1. enable the ``Fetch`` domain with ``handle_auth_requests=True``,
|
|
8
|
+
2. answer every ``Fetch.authRequired`` with the ProxyHat targeting username +
|
|
9
|
+
sub-user password via ``Fetch.continueWithAuth`` (``ProvideCredentials``),
|
|
10
|
+
3. resume every other paused request with ``Fetch.continueRequest``.
|
|
11
|
+
|
|
12
|
+
Enabling ``Fetch`` pauses *every* request (a ``requestPaused`` event), so the
|
|
13
|
+
non-auth requests must be continued too or the page hangs waiting on us.
|
|
14
|
+
|
|
15
|
+
This is the HTTP gateway path (port 8080) — CDP proxy auth answers the HTTP
|
|
16
|
+
proxy's basic-auth challenge. ``nodriver`` is imported lazily so importing this
|
|
17
|
+
module never requires it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from nodriver import Tab
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def enable_proxy_auth(tab: Tab, username: str, password: str) -> None:
|
|
29
|
+
"""Register the CDP proxy-auth handlers on ``tab`` and enable ``Fetch``.
|
|
30
|
+
|
|
31
|
+
Adds a ``Fetch.authRequired`` handler that answers with ``username`` /
|
|
32
|
+
``password`` and a ``Fetch.requestPaused`` handler that resumes normal
|
|
33
|
+
requests, then enables the Fetch domain with auth handling. Call once per tab
|
|
34
|
+
before navigating so the very first request is authenticated.
|
|
35
|
+
"""
|
|
36
|
+
from nodriver import cdp
|
|
37
|
+
|
|
38
|
+
async def _on_auth_required(event: cdp.fetch.AuthRequired) -> None:
|
|
39
|
+
await tab.send(
|
|
40
|
+
cdp.fetch.continue_with_auth(
|
|
41
|
+
request_id=event.request_id,
|
|
42
|
+
auth_challenge_response=cdp.network.AuthChallengeResponse(
|
|
43
|
+
response="ProvideCredentials",
|
|
44
|
+
username=username,
|
|
45
|
+
password=password,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def _on_request_paused(event: cdp.fetch.RequestPaused) -> None:
|
|
51
|
+
await tab.send(cdp.fetch.continue_request(request_id=event.request_id))
|
|
52
|
+
|
|
53
|
+
tab.add_handler(cdp.fetch.AuthRequired, _on_auth_required)
|
|
54
|
+
tab.add_handler(cdp.fetch.RequestPaused, _on_request_paused)
|
|
55
|
+
await tab.send(cdp.fetch.enable(handle_auth_requests=True))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Gateway credential resolution for nodriver-proxyhat.
|
|
2
|
+
|
|
3
|
+
Mirrors the other ProxyHat integrations: explicit ``username``/``password``
|
|
4
|
+
(or ``PROXYHAT_USERNAME``/``PROXYHAT_PASSWORD``) win; otherwise an API key
|
|
5
|
+
(``PROXYHAT_API_KEY``) looks up your sub-users via the official ``proxyhat`` SDK
|
|
6
|
+
and picks an active one with remaining traffic — or the one named by ``sub_user``.
|
|
7
|
+
Everything here except the sub-user lookup is offline.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from proxyhat import ProxyHat
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProxyHatConfigError(RuntimeError):
|
|
18
|
+
"""Raised when ProxyHat credentials cannot be resolved."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _env(name: str) -> str | None:
|
|
22
|
+
value = os.environ.get(name)
|
|
23
|
+
return value.strip() if value and value.strip() else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_credentials(
|
|
27
|
+
*,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
username: str | None = None,
|
|
30
|
+
password: str | None = None,
|
|
31
|
+
sub_user: str | None = None,
|
|
32
|
+
) -> tuple[str, str]:
|
|
33
|
+
"""Resolve a sub-user's ``(proxy_username, proxy_password)``.
|
|
34
|
+
|
|
35
|
+
Options win over environment variables. Precedence:
|
|
36
|
+
|
|
37
|
+
1. explicit ``username`` + ``password`` (``PROXYHAT_USERNAME`` / ``PROXYHAT_PASSWORD``)
|
|
38
|
+
2. ``api_key`` (``PROXYHAT_API_KEY``) → auto-pick an active sub-user, or the
|
|
39
|
+
one named by ``sub_user`` (``PROXYHAT_SUBUSER``).
|
|
40
|
+
"""
|
|
41
|
+
username = username or _env("PROXYHAT_USERNAME")
|
|
42
|
+
password = password or _env("PROXYHAT_PASSWORD")
|
|
43
|
+
if username and password:
|
|
44
|
+
return username, password
|
|
45
|
+
|
|
46
|
+
api_key = api_key or _env("PROXYHAT_API_KEY")
|
|
47
|
+
if not api_key:
|
|
48
|
+
raise ProxyHatConfigError(
|
|
49
|
+
"nodriver-proxyhat: no credentials. Pass api_key (or PROXYHAT_API_KEY), "
|
|
50
|
+
"or username + password (PROXYHAT_USERNAME / PROXYHAT_PASSWORD)."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return _resolve_sub_user(api_key, sub_user or _env("PROXYHAT_SUBUSER"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resolve_sub_user(api_key: str, want: str | None) -> tuple[str, str]:
|
|
57
|
+
users = ProxyHat(api_key=api_key).sub_users.list()
|
|
58
|
+
usable = [u for u in users if not u.suspended_at and (u.traffic_limit == 0 or u.used_traffic < u.traffic_limit)]
|
|
59
|
+
if want:
|
|
60
|
+
chosen = next((u for u in users if u.uuid == want or u.name == want), None)
|
|
61
|
+
else:
|
|
62
|
+
chosen = usable[0] if usable else None
|
|
63
|
+
|
|
64
|
+
if chosen is None or not chosen.proxy_username or not chosen.proxy_password:
|
|
65
|
+
raise ProxyHatConfigError(
|
|
66
|
+
f'nodriver-proxyhat: no sub-user matched "{want}" (or it has no proxy credentials).'
|
|
67
|
+
if want
|
|
68
|
+
else "nodriver-proxyhat: no usable sub-user found (all suspended or out of traffic). "
|
|
69
|
+
"Create one, top up, or pass sub_user."
|
|
70
|
+
)
|
|
71
|
+
return chosen.proxy_username, chosen.proxy_password
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Route a nodriver (undetected Chrome) browser through the ProxyHat gateway.
|
|
2
|
+
|
|
3
|
+
nodriver launches Chrome with ``nodriver.start(browser_args=[...])``, but Chrome's
|
|
4
|
+
``--proxy-server`` flag can't carry a username/password. nodriver gives raw CDP
|
|
5
|
+
access, so the credentialed residential gateway is authenticated the CDP way:
|
|
6
|
+
enable the Fetch domain with ``handle_auth_requests=True`` and answer
|
|
7
|
+
``Fetch.authRequired`` with the ProxyHat targeting username + sub-user password
|
|
8
|
+
(see :mod:`nodriver_proxyhat._auth`).
|
|
9
|
+
|
|
10
|
+
Three entry points:
|
|
11
|
+
|
|
12
|
+
- ``proxyhat_browser_args()`` — the ``--proxy-server`` launch flag for the gateway.
|
|
13
|
+
- ``proxyhat_auth()`` — the ``(username, password)`` for wiring the CDP auth
|
|
14
|
+
handler yourself.
|
|
15
|
+
- ``proxyhat_browser()`` — start Chrome with the flag applied *and* the CDP auth
|
|
16
|
+
handler already wired.
|
|
17
|
+
|
|
18
|
+
This is the HTTP gateway (port 8080); CDP proxy auth applies to the HTTP proxy.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
from proxyhat import (
|
|
26
|
+
PROXYHAT_GATEWAY,
|
|
27
|
+
PROXYHAT_PORT_HTTP,
|
|
28
|
+
build_proxy_username,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from nodriver_proxyhat._auth import enable_proxy_auth
|
|
32
|
+
from nodriver_proxyhat._resolve import resolve_credentials
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from nodriver import Browser
|
|
36
|
+
|
|
37
|
+
# A browser session takes many steps against the same site, so pin one
|
|
38
|
+
# residential IP for the whole session by default — keeps cookies and
|
|
39
|
+
# fingerprint consistent. sticky=False rotates a fresh IP per connection.
|
|
40
|
+
DEFAULT_STICKY = "30m"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def proxyhat_browser_args(**_targeting: Any) -> list[str]:
|
|
44
|
+
"""Return the Chrome launch args pointing nodriver at the ProxyHat gateway.
|
|
45
|
+
|
|
46
|
+
Just ``["--proxy-server=gate.proxyhat.com:8080"]`` — the HTTP gateway host and
|
|
47
|
+
port. Targeting (``country`` / ``region`` / ``city`` / ``sticky`` / ``filter``)
|
|
48
|
+
is *not* encoded here, because Chrome's ``--proxy-server`` can't carry it: it
|
|
49
|
+
lives in the CDP auth username built by :func:`proxyhat_auth` and applied by
|
|
50
|
+
:func:`proxyhat_browser`. Targeting keyword arguments are accepted (so you can
|
|
51
|
+
splat the same dict you pass to those) but do not change the returned flag.
|
|
52
|
+
|
|
53
|
+
Merge the result into your own ``nodriver.start(browser_args=[...])`` call and
|
|
54
|
+
wire auth yourself with :func:`proxyhat_auth` + ``enable_proxy_auth`` — or just
|
|
55
|
+
use :func:`proxyhat_browser`, which does both.
|
|
56
|
+
"""
|
|
57
|
+
return [f"--proxy-server={PROXYHAT_GATEWAY}:{PROXYHAT_PORT_HTTP}"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def proxyhat_auth(
|
|
61
|
+
*,
|
|
62
|
+
api_key: str | None = None,
|
|
63
|
+
username: str | None = None,
|
|
64
|
+
password: str | None = None,
|
|
65
|
+
sub_user: str | None = None,
|
|
66
|
+
country: str | None = None,
|
|
67
|
+
region: str | None = None,
|
|
68
|
+
city: str | None = None,
|
|
69
|
+
sticky: bool | str | None = DEFAULT_STICKY,
|
|
70
|
+
filter: str | None = None,
|
|
71
|
+
) -> tuple[str, str]:
|
|
72
|
+
"""Resolve the ``(gateway_username, password)`` for the ProxyHat gateway.
|
|
73
|
+
|
|
74
|
+
Resolves credentials (``api_key`` / ``PROXYHAT_API_KEY`` auto-picks an active
|
|
75
|
+
sub-user, or pass explicit ``username`` / ``password``), then builds the
|
|
76
|
+
targeting username — geo and stickiness reflected — with the official
|
|
77
|
+
``proxyhat`` SDK grammar. Feed the pair to a CDP ``Fetch.authRequired`` handler
|
|
78
|
+
(``ProvideCredentials``) if you're wiring nodriver yourself; otherwise
|
|
79
|
+
:func:`proxyhat_browser` does it for you.
|
|
80
|
+
|
|
81
|
+
Sticky vs rotating:
|
|
82
|
+
|
|
83
|
+
- ``sticky="30m"`` (default) or ``sticky=True`` pins one residential IP for
|
|
84
|
+
the session — recommended for a browser.
|
|
85
|
+
- ``sticky=False`` (or ``None``) rotates: a fresh residential IP per connection.
|
|
86
|
+
- ``sticky="2h"`` sets a custom session lifetime.
|
|
87
|
+
|
|
88
|
+
Geo/quality targeting: ``country`` (ISO code or ``"any"``), ``region``,
|
|
89
|
+
``city``, ``filter`` (AI IP-quality tier).
|
|
90
|
+
"""
|
|
91
|
+
user, pw = resolve_credentials(
|
|
92
|
+
api_key=api_key,
|
|
93
|
+
username=username,
|
|
94
|
+
password=password,
|
|
95
|
+
sub_user=sub_user,
|
|
96
|
+
)
|
|
97
|
+
# Build the targeting username once so a sticky session mints a single sid
|
|
98
|
+
# shared by every request the browser makes.
|
|
99
|
+
gateway_username = build_proxy_username(
|
|
100
|
+
user,
|
|
101
|
+
country=country,
|
|
102
|
+
region=region,
|
|
103
|
+
city=city,
|
|
104
|
+
sticky=sticky,
|
|
105
|
+
filter=filter,
|
|
106
|
+
)
|
|
107
|
+
return gateway_username, pw
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def proxyhat_browser(
|
|
111
|
+
*,
|
|
112
|
+
api_key: str | None = None,
|
|
113
|
+
username: str | None = None,
|
|
114
|
+
password: str | None = None,
|
|
115
|
+
sub_user: str | None = None,
|
|
116
|
+
country: str | None = None,
|
|
117
|
+
region: str | None = None,
|
|
118
|
+
city: str | None = None,
|
|
119
|
+
sticky: bool | str | None = DEFAULT_STICKY,
|
|
120
|
+
filter: str | None = None,
|
|
121
|
+
**start_kwargs: Any,
|
|
122
|
+
) -> Browser:
|
|
123
|
+
"""Start a nodriver Chrome routed through ProxyHat with gateway auth wired.
|
|
124
|
+
|
|
125
|
+
Resolves credentials, calls ``nodriver.start(browser_args=[...], **start_kwargs)``
|
|
126
|
+
with the ``--proxy-server`` flag applied, then enables CDP ``Fetch`` on the
|
|
127
|
+
browser's main tab and registers a ``Fetch.authRequired`` handler that answers
|
|
128
|
+
with the ProxyHat targeting username + sub-user password. Returns the started
|
|
129
|
+
``Browser`` — navigate with ``await browser.get(url)``.
|
|
130
|
+
|
|
131
|
+
Any extra keyword arguments (``headless``, ``user_data_dir``, ``sandbox``, …)
|
|
132
|
+
are forwarded to ``nodriver.start``; if you pass your own ``browser_args`` they
|
|
133
|
+
are merged with the ProxyHat flag. Sticky by default (one pinned IP for the
|
|
134
|
+
whole session); pass ``sticky=False`` for a rotating IP. See :func:`proxyhat_auth`
|
|
135
|
+
for the full targeting keyword set.
|
|
136
|
+
|
|
137
|
+
``nodriver`` is imported lazily — install it (``pip install nodriver-proxyhat[nodriver]``)
|
|
138
|
+
to use this helper; :func:`proxyhat_browser_args` and :func:`proxyhat_auth` work
|
|
139
|
+
without it.
|
|
140
|
+
"""
|
|
141
|
+
gateway_username, pw = proxyhat_auth(
|
|
142
|
+
api_key=api_key,
|
|
143
|
+
username=username,
|
|
144
|
+
password=password,
|
|
145
|
+
sub_user=sub_user,
|
|
146
|
+
country=country,
|
|
147
|
+
region=region,
|
|
148
|
+
city=city,
|
|
149
|
+
sticky=sticky,
|
|
150
|
+
filter=filter,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
caller_args = start_kwargs.pop("browser_args", None) or []
|
|
154
|
+
browser_args = proxyhat_browser_args() + list(caller_args)
|
|
155
|
+
|
|
156
|
+
# Imported lazily: nodriver is an optional (peer) dependency, so importing
|
|
157
|
+
# this module never forces a Chrome/nodriver install.
|
|
158
|
+
import nodriver
|
|
159
|
+
|
|
160
|
+
browser = await nodriver.start(browser_args=browser_args, **start_kwargs)
|
|
161
|
+
await enable_proxy_auth(browser.main_tab, gateway_username, pw)
|
|
162
|
+
return browser
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nodriver-proxyhat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ProxyHat residential proxies for nodriver (undetected Chrome) — CDP proxy auth, sticky sessions, geo-targeting, rotation.
|
|
5
|
+
Project-URL: Homepage, https://proxyhat.com
|
|
6
|
+
Project-URL: Documentation, https://docs.proxyhat.com
|
|
7
|
+
Project-URL: Repository, https://github.com/ProxyHatCom/nodriver-proxyhat
|
|
8
|
+
Author-email: ProxyHat <support@proxyhat.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: browser-automation,cdp,nodriver,proxy,proxyhat,residential-proxy,sticky-session,undetected-chromedriver,web-scraping
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: proxyhat>=0.2.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
26
|
+
Provides-Extra: nodriver
|
|
27
|
+
Requires-Dist: nodriver>=0.32; extra == 'nodriver'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# nodriver-proxyhat
|
|
31
|
+
|
|
32
|
+
Route [nodriver](https://github.com/ultrafunkamsterdam/nodriver) — the successor to undetected-chromedriver — through [ProxyHat](https://proxyhat.com?utm_source=github&utm_medium=readme&utm_campaign=nodriver) residential proxies. **Authenticated** gateway proxies wired straight into nodriver over CDP, plus a sticky residential IP pinned for the whole session, geo-targeting, and rotation.
|
|
33
|
+
|
|
34
|
+
[](https://github.com/ProxyHatCom/nodriver-proxyhat/actions/workflows/ci.yml)
|
|
35
|
+
[](https://github.com/ProxyHatCom/nodriver-proxyhat/actions/workflows/compat.yml)
|
|
36
|
+
[](https://pypi.org/project/nodriver-proxyhat/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
Chrome's `--proxy-server` flag **can't carry a username and password**, so pointing an undetected browser at a credentialed residential gateway normally means a manual CDP dance. And running a real browser from a datacenter IP gets flagged, CAPTCHA-walled, and blocked anyway — exactly what you're using nodriver to avoid.
|
|
42
|
+
|
|
43
|
+
`nodriver-proxyhat` fixes both. It plugs ProxyHat's residential IPs (50M+ across 148+ countries) into nodriver and handles gateway auth the CDP way nodriver is built for: it enables the **Fetch** domain and answers the proxy's `Fetch.authRequired` challenge with your credentials. One pinned residential IP per session by default, so cookies and fingerprint stay consistent while your script works.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install nodriver-proxyhat nodriver
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`nodriver` is a peer dependency — bring your own version (and its Chrome). The `proxyhat_browser_args()` / `proxyhat_auth()` helpers work without it; `proxyhat_browser()` needs nodriver installed.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import nodriver as uc
|
|
57
|
+
from nodriver_proxyhat import proxyhat_browser
|
|
58
|
+
|
|
59
|
+
async def main():
|
|
60
|
+
# An API key (PROXYHAT_API_KEY) auto-selects an active residential sub-user:
|
|
61
|
+
browser = await proxyhat_browser(country="us") # sticky US IP for the whole session
|
|
62
|
+
page = await browser.get("https://httpbin.org/ip")
|
|
63
|
+
print(await page.get_content())
|
|
64
|
+
browser.stop()
|
|
65
|
+
|
|
66
|
+
uc.loop().run_until_complete(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Get an API key at [proxyhat.com](https://proxyhat.com?utm_source=github&utm_medium=readme&utm_campaign=nodriver).
|
|
70
|
+
|
|
71
|
+
`proxyhat_browser(...)` calls `nodriver.start(...)` for you and forwards any extra keyword arguments (`headless`, `user_data_dir`, `browser_args`, …) unchanged.
|
|
72
|
+
|
|
73
|
+
## Credentials
|
|
74
|
+
|
|
75
|
+
Pass them explicitly or via environment variables — options win over env:
|
|
76
|
+
|
|
77
|
+
| Option | Env var | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `api_key` | `PROXYHAT_API_KEY` | Auto-selects an active sub-user with remaining traffic |
|
|
80
|
+
| `sub_user` | `PROXYHAT_SUBUSER` | Pick a specific sub-user by uuid or name (with an API key) |
|
|
81
|
+
| `username` | `PROXYHAT_USERNAME` | Explicit gateway `proxy_username` (skips the API) |
|
|
82
|
+
| `password` | `PROXYHAT_PASSWORD` | Explicit gateway `proxy_password` |
|
|
83
|
+
|
|
84
|
+
## Targeting
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
await proxyhat_browser(
|
|
88
|
+
country="us", # ISO code or "any" (default)
|
|
89
|
+
region="california",
|
|
90
|
+
city="new_york",
|
|
91
|
+
filter="high", # AI IP-quality tier
|
|
92
|
+
sticky="30m", # session lifetime (default); sticky=False rotates every request
|
|
93
|
+
headless=True, # any extra kwarg is forwarded to nodriver.start
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The same targeting keyword arguments work on `proxyhat_auth(...)`.
|
|
98
|
+
|
|
99
|
+
### Sticky IP per session (default)
|
|
100
|
+
|
|
101
|
+
A browser session takes many steps against the same site — logging in, clicking, scrolling. If the exit IP changed mid-session the site would see a user teleporting between cities and block it. So this package is **sticky by default**: one residential IP is pinned for the whole session (`sticky="30m"`, renewed as you work), keeping cookies and fingerprint coherent.
|
|
102
|
+
|
|
103
|
+
Want a fresh IP on **every** request instead (e.g. many independent one-shot fetches)? Turn stickiness off:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
await proxyhat_browser(country="us", sticky=False) # rotating residential IP per connection
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Set a custom lifetime with `sticky="2h"`.
|
|
110
|
+
|
|
111
|
+
## How authentication works
|
|
112
|
+
|
|
113
|
+
nodriver takes the proxy host/port from `--proxy-server=gate.proxyhat.com:8080`, but a residential gateway also needs a username (the ProxyHat targeting string) and password. Since the flag can't carry them, `proxyhat_browser` uses nodriver's raw Chrome DevTools Protocol access instead:
|
|
114
|
+
|
|
115
|
+
1. it enables the **Fetch** domain on the main tab with `handle_auth_requests=True`;
|
|
116
|
+
2. it registers a `Fetch.authRequired` handler that answers with your targeting username + sub-user password via `Fetch.continueWithAuth` (`ProvideCredentials`);
|
|
117
|
+
3. it resumes every other paused request with `Fetch.continueRequest` (enabling Fetch pauses all requests, so non-auth ones must be continued too).
|
|
118
|
+
|
|
119
|
+
The targeting username (e.g. `<user>-country-us-sid-<id>-ttl-30m`) is built by the official [`proxyhat`](https://pypi.org/project/proxyhat/) SDK, so a sticky session mints a single session id shared across the run.
|
|
120
|
+
|
|
121
|
+
This is the **HTTP gateway** (port 8080) — CDP proxy auth answers the HTTP proxy's basic-auth challenge.
|
|
122
|
+
|
|
123
|
+
## Wiring it yourself
|
|
124
|
+
|
|
125
|
+
Prefer to drive `nodriver.start()` your way? Grab the launch flag and the resolved credentials and wire the CDP handler yourself:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import nodriver as uc
|
|
129
|
+
from nodriver import cdp
|
|
130
|
+
from nodriver_proxyhat import proxyhat_auth, proxyhat_browser_args, enable_proxy_auth
|
|
131
|
+
|
|
132
|
+
async def main():
|
|
133
|
+
username, password = proxyhat_auth(country="de", sticky="1h")
|
|
134
|
+
|
|
135
|
+
browser = await uc.start(browser_args=proxyhat_browser_args() + ["--headless=new"])
|
|
136
|
+
# enable_proxy_auth does the Fetch.enable + authRequired dance for you:
|
|
137
|
+
await enable_proxy_auth(browser.main_tab, username, password)
|
|
138
|
+
|
|
139
|
+
page = await browser.get("https://httpbin.org/ip")
|
|
140
|
+
print(await page.get_content())
|
|
141
|
+
browser.stop()
|
|
142
|
+
|
|
143
|
+
uc.loop().run_until_complete(main())
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`proxyhat_browser_args()` returns `["--proxy-server=gate.proxyhat.com:8080"]`; `proxyhat_auth()` returns the `(username, password)` for a `Fetch.authRequired` handler.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT © [ProxyHat](https://proxyhat.com)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
nodriver_proxyhat/__init__.py,sha256=tHzV8AnV96Emh-Iqg3u9Kw6E_zSqu1h4Dum5qwDI2xE,517
|
|
2
|
+
nodriver_proxyhat/_auth.py,sha256=RKd1v968NI6cvKyGUd6qoQxI91uUdsBixCQiXshHPE8,2345
|
|
3
|
+
nodriver_proxyhat/_resolve.py,sha256=tfsq3e743i7zOAMxSXZm1T0l630xAe_OIIqi0zQaHHE,2680
|
|
4
|
+
nodriver_proxyhat/proxy.py,sha256=uDZ_wLrN31MOTJWxe8BifmlfjVGwzSEXR2hkKw60VrA,6364
|
|
5
|
+
nodriver_proxyhat-0.1.0.dist-info/METADATA,sha256=77XI-LaRzcYO7wg6rwEbat-qzLa4_pmvmbxPNJPxyv8,7587
|
|
6
|
+
nodriver_proxyhat-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
7
|
+
nodriver_proxyhat-0.1.0.dist-info/licenses/LICENSE,sha256=_yoC1-ij-yJ3LPoEUetu723duo_t9XcI08QcSS6jSiQ,1065
|
|
8
|
+
nodriver_proxyhat-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ProxyHat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|