sherlock-proxyhat 0.1.0__tar.gz
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.
- sherlock_proxyhat-0.1.0/.github/workflows/ci.yml +60 -0
- sherlock_proxyhat-0.1.0/.github/workflows/compat.yml +58 -0
- sherlock_proxyhat-0.1.0/.gitignore +8 -0
- sherlock_proxyhat-0.1.0/LICENSE +21 -0
- sherlock_proxyhat-0.1.0/PKG-INFO +139 -0
- sherlock_proxyhat-0.1.0/README.md +108 -0
- sherlock_proxyhat-0.1.0/examples/basic.sh +17 -0
- sherlock_proxyhat-0.1.0/pyproject.toml +64 -0
- sherlock_proxyhat-0.1.0/renovate.json +19 -0
- sherlock_proxyhat-0.1.0/src/sherlock_proxyhat/__init__.py +13 -0
- sherlock_proxyhat-0.1.0/src/sherlock_proxyhat/_resolve.py +71 -0
- sherlock_proxyhat-0.1.0/src/sherlock_proxyhat/cli.py +203 -0
- sherlock_proxyhat-0.1.0/tests/test_cli.py +107 -0
- sherlock_proxyhat-0.1.0/tests/test_url.py +101 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: pip install ruff
|
|
19
|
+
- run: ruff check src/ tests/
|
|
20
|
+
- run: ruff format --check src/ tests/
|
|
21
|
+
|
|
22
|
+
test:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
strategy:
|
|
25
|
+
matrix:
|
|
26
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
# Tests fake the `sherlock` executable and subprocess, so [dev] alone
|
|
33
|
+
# (no sherlock-project install) is enough.
|
|
34
|
+
- run: pip install -e ".[dev]"
|
|
35
|
+
- run: pytest -v
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
- run: pip install build
|
|
45
|
+
- run: python -m build
|
|
46
|
+
|
|
47
|
+
publish:
|
|
48
|
+
needs: [lint, test, build]
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
51
|
+
permissions:
|
|
52
|
+
id-token: write
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-python@v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.12"
|
|
58
|
+
- run: pip install build
|
|
59
|
+
- run: python -m build
|
|
60
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Compat (sherlock-project latest)
|
|
2
|
+
|
|
3
|
+
# Autonomy watchdog: weekly (and on demand) it installs the newest sherlock-project
|
|
4
|
+
# and our newest proxyhat SDK, then checks that Sherlock still exposes the `--proxy`
|
|
5
|
+
# flag this launcher depends on, and runs our tests. A red run means upstream
|
|
6
|
+
# changed the proxy CLI surface — it opens/updates a tracking issue so we notice
|
|
7
|
+
# before users do. The proactive path is Renovate (renovate.json), which opens a
|
|
8
|
+
# version-bump PR that must pass CI before merging.
|
|
9
|
+
on:
|
|
10
|
+
schedule:
|
|
11
|
+
- cron: "0 6 * * 1" # Mondays 06:00 UTC
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
issues: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
compat:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
- run: pip install -e ".[dev]"
|
|
27
|
+
- name: Install latest upstreams
|
|
28
|
+
run: pip install --upgrade sherlock-project proxyhat
|
|
29
|
+
- name: Assert Sherlock still accepts --proxy
|
|
30
|
+
# This launcher forwards a residential proxy URL via `sherlock --proxy <url>`.
|
|
31
|
+
# Fail loudly if that flag ever disappears or is renamed.
|
|
32
|
+
run: |
|
|
33
|
+
sherlock --help 2>&1 | grep -- '--proxy' \
|
|
34
|
+
|| { echo "sherlock no longer advertises a --proxy flag"; exit 1; }
|
|
35
|
+
echo "sherlock --proxy surface OK"
|
|
36
|
+
- run: pytest -v
|
|
37
|
+
- name: Open an issue on failure
|
|
38
|
+
if: failure()
|
|
39
|
+
uses: actions/github-script@v7
|
|
40
|
+
with:
|
|
41
|
+
script: |
|
|
42
|
+
const title = "Compat break: sherlock-project@latest / proxyhat@latest";
|
|
43
|
+
const { data: issues } = await github.rest.issues.listForRepo({
|
|
44
|
+
owner: context.repo.owner, repo: context.repo.repo,
|
|
45
|
+
state: "open", labels: "compat",
|
|
46
|
+
});
|
|
47
|
+
const body = `The weekly compatibility check failed against the latest upstream releases.\n\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
48
|
+
if (issues.length === 0) {
|
|
49
|
+
await github.rest.issues.create({
|
|
50
|
+
owner: context.repo.owner, repo: context.repo.repo,
|
|
51
|
+
title, body, labels: ["compat"],
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
await github.rest.issues.createComment({
|
|
55
|
+
owner: context.repo.owner, repo: context.repo.repo,
|
|
56
|
+
issue_number: issues[0].number, body,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -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.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sherlock-proxyhat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run the Sherlock OSINT username hunter through ProxyHat residential proxies — rotating IPs, geo-targeting, fewer blocks.
|
|
5
|
+
Project-URL: Homepage, https://proxyhat.com
|
|
6
|
+
Project-URL: Documentation, https://docs.proxyhat.com
|
|
7
|
+
Project-URL: Repository, https://github.com/ProxyHatCom/sherlock-proxyhat
|
|
8
|
+
Author-email: ProxyHat <support@proxyhat.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: osint,proxy,proxyhat,reconnaissance,residential-proxy,rotating-proxy,sherlock,username
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: proxyhat>=0.2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: sherlock
|
|
29
|
+
Requires-Dist: sherlock-project>=0.14; extra == 'sherlock'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# sherlock-proxyhat
|
|
33
|
+
|
|
34
|
+
Run [Sherlock](https://github.com/sherlock-project/sherlock) — the OSINT username hunter — through [ProxyHat](https://proxyhat.com?utm_source=github&utm_medium=readme&utm_campaign=sherlock) residential proxies. Rotating residential IPs by default (a fresh IP per connection across the hundreds of sites Sherlock checks), plus geo-targeting and anonymity, so you get rate-limited and blocked far less.
|
|
35
|
+
|
|
36
|
+
[](https://github.com/ProxyHatCom/sherlock-proxyhat/actions/workflows/ci.yml)
|
|
37
|
+
[](https://github.com/ProxyHatCom/sherlock-proxyhat/actions/workflows/compat.yml)
|
|
38
|
+
[](https://pypi.org/project/sherlock-proxyhat/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
## What this is
|
|
42
|
+
|
|
43
|
+
Sherlock is an **app**, not a library: you run `sherlock <username>` and it checks that name across 400+ sites, accepting a single `--proxy <url>` for all of them. Hammering that many sites from one datacenter IP gets throttled and blocked fast.
|
|
44
|
+
|
|
45
|
+
`sherlock-proxyhat` is a **thin launcher**. It resolves a ProxyHat residential gateway URL, then runs the real `sherlock` executable with `--proxy <url>` prepended — every other argument (usernames and Sherlock's own flags) passes straight through. No fork, no patched Sherlock: it just wires ProxyHat into the `--proxy` flag Sherlock already has.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install sherlock-proxyhat sherlock-project
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`sherlock-project` (which provides the `sherlock` command) is an optional dependency — this launcher shells out to the `sherlock` executable, so any install works (pip, pipx, system package). To pull it in with the launcher:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install "sherlock-proxyhat[sherlock]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# An API key auto-selects an active residential sub-user:
|
|
63
|
+
export PROXYHAT_API_KEY=ph_xxx
|
|
64
|
+
|
|
65
|
+
# Same arguments as sherlock — just run it through the launcher:
|
|
66
|
+
sherlock-proxyhat johndoe
|
|
67
|
+
|
|
68
|
+
# Geo-target and add sherlock's own flags; everything after is passed through:
|
|
69
|
+
sherlock-proxyhat --proxyhat-country us johndoe janedoe --timeout 30 --csv
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Get an API key at [proxyhat.com](https://proxyhat.com?utm_source=github&utm_medium=readme&utm_campaign=sherlock).
|
|
73
|
+
|
|
74
|
+
Anything that isn't a `--proxyhat-*` option is forwarded to `sherlock` verbatim, so every Sherlock flag (`--timeout`, `--site`, `--nsfw`, `--csv`, `-o`, …) works unchanged.
|
|
75
|
+
|
|
76
|
+
## Credentials
|
|
77
|
+
|
|
78
|
+
Pass them as `--proxyhat-*` flags or via environment variables — flags win over env:
|
|
79
|
+
|
|
80
|
+
| Flag | Env var | Notes |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `--proxyhat-api-key` | `PROXYHAT_API_KEY` | Auto-selects an active sub-user with remaining traffic |
|
|
83
|
+
| `--proxyhat-sub-user` | `PROXYHAT_SUBUSER` | Pick a specific sub-user by uuid or name (with an API key) |
|
|
84
|
+
| `--proxyhat-username` | `PROXYHAT_USERNAME` | Explicit gateway `proxy_username` (skips the API) |
|
|
85
|
+
| `--proxyhat-password` | `PROXYHAT_PASSWORD` | Explicit gateway `proxy_password` |
|
|
86
|
+
|
|
87
|
+
## Targeting
|
|
88
|
+
|
|
89
|
+
All targeting lives on `--proxyhat-*` flags, which are stripped before the command reaches `sherlock`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
sherlock-proxyhat \
|
|
93
|
+
--proxyhat-protocol http \ # or socks5
|
|
94
|
+
--proxyhat-country us \ # ISO code or "any" (default)
|
|
95
|
+
--proxyhat-region california \
|
|
96
|
+
--proxyhat-city los_angeles \
|
|
97
|
+
--proxyhat-filter high \ # AI IP-quality tier
|
|
98
|
+
johndoe
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Rotating IPs by default
|
|
102
|
+
|
|
103
|
+
Sherlock reuses one proxy for the whole run, so this launcher builds a **rotating** gateway username by default: the ProxyHat gateway hands out a fresh residential IP on every connection, spreading the hundreds of per-site checks across many IPs — exactly what you want to dodge per-IP rate limits during a scan.
|
|
104
|
+
|
|
105
|
+
Want to pin a single IP for the whole run instead? Add `--proxyhat-sticky` (optionally with a lifetime via `--proxyhat-sticky-ttl`):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
sherlock-proxyhat --proxyhat-sticky johndoe # one pinned IP, 30m default
|
|
109
|
+
sherlock-proxyhat --proxyhat-sticky-ttl 2h johndoe # one pinned IP, 2h lifetime
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Bring your own proxy
|
|
113
|
+
|
|
114
|
+
If you pass Sherlock's own `--proxy`/`-p` (or `--tor`) yourself, the launcher steps aside: it forwards your arguments unchanged and does not inject a ProxyHat URL.
|
|
115
|
+
|
|
116
|
+
## Using the URL builder directly
|
|
117
|
+
|
|
118
|
+
Need the gateway URL in your own script or a different tool? Import the helper:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from sherlock_proxyhat import proxyhat_proxy_url
|
|
122
|
+
|
|
123
|
+
url = proxyhat_proxy_url(api_key="ph_xxx", country="us")
|
|
124
|
+
# -> "http://<user>-country-us:<pass>@gate.proxyhat.com:8080"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Rotating by default (`sticky=None`); pass `sticky="30m"` (or `True`) to pin one IP.
|
|
128
|
+
|
|
129
|
+
## How it works
|
|
130
|
+
|
|
131
|
+
The launcher parses out its `--proxyhat-*` options, resolves your gateway credentials via the official [`proxyhat`](https://pypi.org/project/proxyhat/) SDK (an API key auto-picks an active sub-user, or pass `username`/`password`), and builds a connection URL carrying ProxyHat's targeting grammar (`http://<user>-country-us:<pass>@gate.proxyhat.com:8080`, or `socks5://…:1080`). It then finds the `sherlock` executable on your `PATH` and runs `sherlock --proxy <url> <your other args>` as a subprocess, returning Sherlock's exit code. A rotating username makes the gateway hand out a fresh residential IP per connection; `--proxyhat-sticky` pins one.
|
|
132
|
+
|
|
133
|
+
## Roadmap
|
|
134
|
+
|
|
135
|
+
This launcher wraps Sherlock's existing `--proxy` flag on purpose — it's the honest, low-friction way to ship today. A **planned follow-up** is an upstream PR to Sherlock adding a native `--proxyhat` flag (resolve credentials and geo-target from within Sherlock itself, no wrapper), so ProxyHat becomes a first-class option in the tool. Until that lands, this package is the supported path.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT © [ProxyHat](https://proxyhat.com)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# sherlock-proxyhat
|
|
2
|
+
|
|
3
|
+
Run [Sherlock](https://github.com/sherlock-project/sherlock) — the OSINT username hunter — through [ProxyHat](https://proxyhat.com?utm_source=github&utm_medium=readme&utm_campaign=sherlock) residential proxies. Rotating residential IPs by default (a fresh IP per connection across the hundreds of sites Sherlock checks), plus geo-targeting and anonymity, so you get rate-limited and blocked far less.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/ProxyHatCom/sherlock-proxyhat/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/ProxyHatCom/sherlock-proxyhat/actions/workflows/compat.yml)
|
|
7
|
+
[](https://pypi.org/project/sherlock-proxyhat/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
## What this is
|
|
11
|
+
|
|
12
|
+
Sherlock is an **app**, not a library: you run `sherlock <username>` and it checks that name across 400+ sites, accepting a single `--proxy <url>` for all of them. Hammering that many sites from one datacenter IP gets throttled and blocked fast.
|
|
13
|
+
|
|
14
|
+
`sherlock-proxyhat` is a **thin launcher**. It resolves a ProxyHat residential gateway URL, then runs the real `sherlock` executable with `--proxy <url>` prepended — every other argument (usernames and Sherlock's own flags) passes straight through. No fork, no patched Sherlock: it just wires ProxyHat into the `--proxy` flag Sherlock already has.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install sherlock-proxyhat sherlock-project
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`sherlock-project` (which provides the `sherlock` command) is an optional dependency — this launcher shells out to the `sherlock` executable, so any install works (pip, pipx, system package). To pull it in with the launcher:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install "sherlock-proxyhat[sherlock]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# An API key auto-selects an active residential sub-user:
|
|
32
|
+
export PROXYHAT_API_KEY=ph_xxx
|
|
33
|
+
|
|
34
|
+
# Same arguments as sherlock — just run it through the launcher:
|
|
35
|
+
sherlock-proxyhat johndoe
|
|
36
|
+
|
|
37
|
+
# Geo-target and add sherlock's own flags; everything after is passed through:
|
|
38
|
+
sherlock-proxyhat --proxyhat-country us johndoe janedoe --timeout 30 --csv
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Get an API key at [proxyhat.com](https://proxyhat.com?utm_source=github&utm_medium=readme&utm_campaign=sherlock).
|
|
42
|
+
|
|
43
|
+
Anything that isn't a `--proxyhat-*` option is forwarded to `sherlock` verbatim, so every Sherlock flag (`--timeout`, `--site`, `--nsfw`, `--csv`, `-o`, …) works unchanged.
|
|
44
|
+
|
|
45
|
+
## Credentials
|
|
46
|
+
|
|
47
|
+
Pass them as `--proxyhat-*` flags or via environment variables — flags win over env:
|
|
48
|
+
|
|
49
|
+
| Flag | Env var | Notes |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `--proxyhat-api-key` | `PROXYHAT_API_KEY` | Auto-selects an active sub-user with remaining traffic |
|
|
52
|
+
| `--proxyhat-sub-user` | `PROXYHAT_SUBUSER` | Pick a specific sub-user by uuid or name (with an API key) |
|
|
53
|
+
| `--proxyhat-username` | `PROXYHAT_USERNAME` | Explicit gateway `proxy_username` (skips the API) |
|
|
54
|
+
| `--proxyhat-password` | `PROXYHAT_PASSWORD` | Explicit gateway `proxy_password` |
|
|
55
|
+
|
|
56
|
+
## Targeting
|
|
57
|
+
|
|
58
|
+
All targeting lives on `--proxyhat-*` flags, which are stripped before the command reaches `sherlock`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sherlock-proxyhat \
|
|
62
|
+
--proxyhat-protocol http \ # or socks5
|
|
63
|
+
--proxyhat-country us \ # ISO code or "any" (default)
|
|
64
|
+
--proxyhat-region california \
|
|
65
|
+
--proxyhat-city los_angeles \
|
|
66
|
+
--proxyhat-filter high \ # AI IP-quality tier
|
|
67
|
+
johndoe
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Rotating IPs by default
|
|
71
|
+
|
|
72
|
+
Sherlock reuses one proxy for the whole run, so this launcher builds a **rotating** gateway username by default: the ProxyHat gateway hands out a fresh residential IP on every connection, spreading the hundreds of per-site checks across many IPs — exactly what you want to dodge per-IP rate limits during a scan.
|
|
73
|
+
|
|
74
|
+
Want to pin a single IP for the whole run instead? Add `--proxyhat-sticky` (optionally with a lifetime via `--proxyhat-sticky-ttl`):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sherlock-proxyhat --proxyhat-sticky johndoe # one pinned IP, 30m default
|
|
78
|
+
sherlock-proxyhat --proxyhat-sticky-ttl 2h johndoe # one pinned IP, 2h lifetime
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Bring your own proxy
|
|
82
|
+
|
|
83
|
+
If you pass Sherlock's own `--proxy`/`-p` (or `--tor`) yourself, the launcher steps aside: it forwards your arguments unchanged and does not inject a ProxyHat URL.
|
|
84
|
+
|
|
85
|
+
## Using the URL builder directly
|
|
86
|
+
|
|
87
|
+
Need the gateway URL in your own script or a different tool? Import the helper:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from sherlock_proxyhat import proxyhat_proxy_url
|
|
91
|
+
|
|
92
|
+
url = proxyhat_proxy_url(api_key="ph_xxx", country="us")
|
|
93
|
+
# -> "http://<user>-country-us:<pass>@gate.proxyhat.com:8080"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Rotating by default (`sticky=None`); pass `sticky="30m"` (or `True`) to pin one IP.
|
|
97
|
+
|
|
98
|
+
## How it works
|
|
99
|
+
|
|
100
|
+
The launcher parses out its `--proxyhat-*` options, resolves your gateway credentials via the official [`proxyhat`](https://pypi.org/project/proxyhat/) SDK (an API key auto-picks an active sub-user, or pass `username`/`password`), and builds a connection URL carrying ProxyHat's targeting grammar (`http://<user>-country-us:<pass>@gate.proxyhat.com:8080`, or `socks5://…:1080`). It then finds the `sherlock` executable on your `PATH` and runs `sherlock --proxy <url> <your other args>` as a subprocess, returning Sherlock's exit code. A rotating username makes the gateway hand out a fresh residential IP per connection; `--proxyhat-sticky` pins one.
|
|
101
|
+
|
|
102
|
+
## Roadmap
|
|
103
|
+
|
|
104
|
+
This launcher wraps Sherlock's existing `--proxy` flag on purpose — it's the honest, low-friction way to ship today. A **planned follow-up** is an upstream PR to Sherlock adding a native `--proxyhat` flag (resolve credentials and geo-target from within Sherlock itself, no wrapper), so ProxyHat becomes a first-class option in the tool. Until that lands, this package is the supported path.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT © [ProxyHat](https://proxyhat.com)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Minimal sherlock-proxyhat example.
|
|
3
|
+
#
|
|
4
|
+
# PROXYHAT_API_KEY=ph_xxx ./examples/basic.sh johndoe
|
|
5
|
+
#
|
|
6
|
+
# Runs Sherlock through a US residential IP, rotating a fresh IP per connection
|
|
7
|
+
# (the default) so the hundreds of per-site checks are spread across many IPs.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
USERNAME="${1:-johndoe}"
|
|
11
|
+
|
|
12
|
+
# api_key defaults to $PROXYHAT_API_KEY and auto-selects an active sub-user.
|
|
13
|
+
# Everything after the --proxyhat-* flags is passed straight to `sherlock`.
|
|
14
|
+
sherlock-proxyhat \
|
|
15
|
+
--proxyhat-country us \
|
|
16
|
+
"$USERNAME" \
|
|
17
|
+
--timeout 30
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sherlock-proxyhat"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Run the Sherlock OSINT username hunter through ProxyHat residential proxies — rotating IPs, geo-targeting, fewer blocks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "ProxyHat", email = "support@proxyhat.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"sherlock",
|
|
15
|
+
"osint",
|
|
16
|
+
"proxy",
|
|
17
|
+
"residential-proxy",
|
|
18
|
+
"proxyhat",
|
|
19
|
+
"username",
|
|
20
|
+
"reconnaissance",
|
|
21
|
+
"rotating-proxy",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Intended Audience :: Information Technology",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Internet :: Proxy Servers",
|
|
34
|
+
"Topic :: Security",
|
|
35
|
+
]
|
|
36
|
+
dependencies = ["proxyhat>=0.2.0"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
sherlock-proxyhat = "sherlock_proxyhat.cli:main"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://proxyhat.com"
|
|
43
|
+
Documentation = "https://docs.proxyhat.com"
|
|
44
|
+
Repository = "https://github.com/ProxyHatCom/sherlock-proxyhat"
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
# Sherlock is the app this launcher shells out to. It's optional because we call
|
|
48
|
+
# the `sherlock` executable via subprocess (never import it), so you can also use
|
|
49
|
+
# a sherlock installed separately (pipx, system package, etc.).
|
|
50
|
+
sherlock = ["sherlock-project>=0.14"]
|
|
51
|
+
dev = ["pytest>=7.0", "ruff>=0.4"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/sherlock_proxyhat"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
target-version = "py311"
|
|
58
|
+
line-length = 120
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended", ":semanticCommits"],
|
|
4
|
+
"labels": ["dependencies"],
|
|
5
|
+
"schedule": ["before 6am on monday"],
|
|
6
|
+
"packageRules": [
|
|
7
|
+
{
|
|
8
|
+
"description": "Auto-merge dev-dependency and upstream bumps once CI (incl. compat) is green.",
|
|
9
|
+
"matchDepTypes": ["build-system.requires", "project.dependencies", "project.optional-dependencies"],
|
|
10
|
+
"automerge": true,
|
|
11
|
+
"automergeType": "pr"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"description": "Track the sherlock-project range but never auto-widen it silently.",
|
|
15
|
+
"matchPackageNames": ["sherlock-project"],
|
|
16
|
+
"automerge": false
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""sherlock-proxyhat — run the Sherlock OSINT tool through ProxyHat residential proxies."""
|
|
2
|
+
|
|
3
|
+
from sherlock_proxyhat._resolve import ProxyHatConfigError, resolve_credentials
|
|
4
|
+
from sherlock_proxyhat.cli import build_command, main, proxyhat_proxy_url
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ProxyHatConfigError",
|
|
8
|
+
"build_command",
|
|
9
|
+
"main",
|
|
10
|
+
"proxyhat_proxy_url",
|
|
11
|
+
"resolve_credentials",
|
|
12
|
+
]
|
|
13
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Gateway credential resolution for sherlock-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
|
+
"sherlock-proxyhat: no credentials. Pass --proxyhat-api-key (or PROXYHAT_API_KEY), "
|
|
50
|
+
"or --proxyhat-username + --proxyhat-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'sherlock-proxyhat: no sub-user matched "{want}" (or it has no proxy credentials).'
|
|
67
|
+
if want
|
|
68
|
+
else "sherlock-proxyhat: no usable sub-user found (all suspended or out of traffic). "
|
|
69
|
+
"Create one, top up, or pass --proxyhat-sub-user."
|
|
70
|
+
)
|
|
71
|
+
return chosen.proxy_username, chosen.proxy_password
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""``sherlock-proxyhat`` — a thin launcher that runs Sherlock through ProxyHat residential proxies.
|
|
2
|
+
|
|
3
|
+
Sherlock (https://github.com/sherlock-project/sherlock) is an OSINT *app*, not a
|
|
4
|
+
library: it hunts a username across hundreds of sites and accepts a single
|
|
5
|
+
``--proxy <url>`` for all of them. Firing that many requests from one datacenter
|
|
6
|
+
IP gets rate-limited and blocked fast, so this launcher resolves a ProxyHat
|
|
7
|
+
residential gateway URL and invokes the real ``sherlock`` executable with
|
|
8
|
+
``--proxy <url>`` prepended, passing every other argument (usernames + flags)
|
|
9
|
+
straight through.
|
|
10
|
+
|
|
11
|
+
Because Sherlock reuses the one proxy for the whole run, we build a **rotating**
|
|
12
|
+
gateway username by default — the ProxyHat gateway then hands out a fresh
|
|
13
|
+
residential IP per connection, spreading the hundreds of site checks across many
|
|
14
|
+
IPs. Pass ``--proxyhat-sticky`` to pin one IP instead.
|
|
15
|
+
|
|
16
|
+
This is deliberately a wrapper around Sherlock's existing ``--proxy`` flag, not a
|
|
17
|
+
fork. A native ``--proxyhat`` flag upstream is a planned follow-up (see README).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
|
|
28
|
+
from proxyhat import build_connection_url
|
|
29
|
+
|
|
30
|
+
from sherlock_proxyhat._resolve import ProxyHatConfigError, resolve_credentials
|
|
31
|
+
|
|
32
|
+
# Sherlock makes one proxy serve the entire multi-site run, so the sensible
|
|
33
|
+
# default is to rotate: a fresh residential IP per connection spreads the load
|
|
34
|
+
# and dodges per-IP rate limits. sticky is opt-in via --proxyhat-sticky.
|
|
35
|
+
DEFAULT_STICKY: bool | str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def proxyhat_proxy_url(
|
|
39
|
+
*,
|
|
40
|
+
api_key: str | None = None,
|
|
41
|
+
username: str | None = None,
|
|
42
|
+
password: str | None = None,
|
|
43
|
+
sub_user: str | None = None,
|
|
44
|
+
country: str | None = None,
|
|
45
|
+
region: str | None = None,
|
|
46
|
+
city: str | None = None,
|
|
47
|
+
sticky: bool | str | None = DEFAULT_STICKY,
|
|
48
|
+
filter: str | None = None,
|
|
49
|
+
protocol: str = "http",
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Return a full ProxyHat gateway proxy URL for Sherlock's ``--proxy``.
|
|
52
|
+
|
|
53
|
+
Resolves credentials (``api_key``/``PROXYHAT_API_KEY`` auto-picks an active
|
|
54
|
+
sub-user, or pass ``username``/``password``) then builds a connection URL like
|
|
55
|
+
``http://<user>-country-us:<pass>@gate.proxyhat.com:8080`` (or ``socks5://…:1080``)
|
|
56
|
+
via the official ``proxyhat`` SDK's targeting grammar.
|
|
57
|
+
|
|
58
|
+
Rotating by default (``sticky=None``): the gateway hands out a fresh
|
|
59
|
+
residential IP per connection. Pass ``sticky="30m"`` (or ``True``) to pin one
|
|
60
|
+
IP for the whole run. Geo/quality: ``country`` (ISO code or ``"any"``),
|
|
61
|
+
``region``, ``city``, ``filter`` (AI IP-quality tier). ``protocol`` is
|
|
62
|
+
``"http"`` or ``"socks5"``.
|
|
63
|
+
"""
|
|
64
|
+
user, pw = resolve_credentials(
|
|
65
|
+
api_key=api_key,
|
|
66
|
+
username=username,
|
|
67
|
+
password=password,
|
|
68
|
+
sub_user=sub_user,
|
|
69
|
+
)
|
|
70
|
+
return build_connection_url(
|
|
71
|
+
username=user,
|
|
72
|
+
password=pw,
|
|
73
|
+
country=country,
|
|
74
|
+
region=region,
|
|
75
|
+
city=city,
|
|
76
|
+
sticky=sticky,
|
|
77
|
+
filter=filter,
|
|
78
|
+
protocol=protocol,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
83
|
+
"""Parser for the ``--proxyhat-*`` options this launcher owns.
|
|
84
|
+
|
|
85
|
+
Everything else (usernames and all of Sherlock's own flags) is left for
|
|
86
|
+
``parse_known_args`` to return untouched, so we never have to track Sherlock's
|
|
87
|
+
argument surface.
|
|
88
|
+
"""
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
prog="sherlock-proxyhat",
|
|
91
|
+
description=(
|
|
92
|
+
"Run Sherlock through ProxyHat residential proxies. All arguments other than "
|
|
93
|
+
"--proxyhat-* are passed straight through to the `sherlock` executable."
|
|
94
|
+
),
|
|
95
|
+
add_help=False, # let --help fall through to sherlock; use --proxyhat-help for ours
|
|
96
|
+
# Never abbreviation-match: a bare `--proxy` (Sherlock's own flag) must fall
|
|
97
|
+
# through to passthrough, not be swallowed as a prefix of `--proxyhat-*`.
|
|
98
|
+
allow_abbrev=False,
|
|
99
|
+
)
|
|
100
|
+
g = parser.add_argument_group("proxyhat options (stripped before forwarding to sherlock)")
|
|
101
|
+
g.add_argument("--proxyhat-help", action="help", help="show this launcher's proxyhat options and exit")
|
|
102
|
+
g.add_argument("--proxyhat-api-key", metavar="KEY", help="ProxyHat API key (or env PROXYHAT_API_KEY)")
|
|
103
|
+
g.add_argument("--proxyhat-username", metavar="USER", help="explicit gateway proxy_username (or PROXYHAT_USERNAME)")
|
|
104
|
+
g.add_argument("--proxyhat-password", metavar="PASS", help="explicit gateway proxy_password (or PROXYHAT_PASSWORD)")
|
|
105
|
+
g.add_argument("--proxyhat-sub-user", metavar="ID", help="pick a sub-user by uuid or name (or PROXYHAT_SUBUSER)")
|
|
106
|
+
g.add_argument("--proxyhat-country", metavar="ISO", help='country ISO code, or "any" (default)')
|
|
107
|
+
g.add_argument("--proxyhat-region", metavar="NAME", help="region/state to target")
|
|
108
|
+
g.add_argument("--proxyhat-city", metavar="NAME", help="city to target")
|
|
109
|
+
g.add_argument("--proxyhat-filter", metavar="TIER", help="AI IP-quality tier (e.g. high)")
|
|
110
|
+
g.add_argument("--proxyhat-protocol", choices=("http", "socks5"), default="http", help="gateway protocol")
|
|
111
|
+
# Two flags rather than one optional-value flag: a bare `--proxyhat-sticky VALUE`
|
|
112
|
+
# would greedily swallow the following positional username as its value, so
|
|
113
|
+
# stickiness is a plain switch and the lifetime is a separate option.
|
|
114
|
+
g.add_argument(
|
|
115
|
+
"--proxyhat-sticky",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="pin one residential IP for the whole run (default: rotating — a fresh IP per connection)",
|
|
118
|
+
)
|
|
119
|
+
g.add_argument(
|
|
120
|
+
"--proxyhat-sticky-ttl",
|
|
121
|
+
metavar="TTL",
|
|
122
|
+
help='sticky session lifetime like "30m"/"2h" (implies --proxyhat-sticky; default 30m)',
|
|
123
|
+
)
|
|
124
|
+
return parser
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolve_sticky(*, sticky: bool, ttl: str | None) -> bool | str | None:
|
|
128
|
+
"""Fold the two sticky flags into the SDK's ``sticky`` argument.
|
|
129
|
+
|
|
130
|
+
A TTL implies sticky and sets the lifetime; a bare ``--proxyhat-sticky`` pins
|
|
131
|
+
with the SDK default (30m); neither means rotating.
|
|
132
|
+
"""
|
|
133
|
+
if ttl:
|
|
134
|
+
return ttl
|
|
135
|
+
if sticky:
|
|
136
|
+
return True
|
|
137
|
+
return DEFAULT_STICKY
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _has_user_proxy(passthrough: Sequence[str]) -> bool:
|
|
141
|
+
"""True if the user already supplied Sherlock's own --proxy/-p flag."""
|
|
142
|
+
return any(a == "--proxy" or a == "-p" or a.startswith("--proxy=") for a in passthrough)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_command(argv: Sequence[str]) -> list[str]:
|
|
146
|
+
"""Resolve the ProxyHat proxy and build the full ``sherlock`` command line.
|
|
147
|
+
|
|
148
|
+
Returns ``[sherlock_exe, "--proxy", <url>, *passthrough]``. If the caller
|
|
149
|
+
already passed Sherlock's own ``--proxy``/``-p``, that wins and no ProxyHat URL
|
|
150
|
+
is injected (a note goes to stderr). Raises ``ProxyHatConfigError`` if
|
|
151
|
+
credentials can't be resolved and ``FileNotFoundError`` if ``sherlock`` isn't
|
|
152
|
+
installed.
|
|
153
|
+
"""
|
|
154
|
+
ns, passthrough = _build_parser().parse_known_args(list(argv))
|
|
155
|
+
|
|
156
|
+
sherlock_exe = shutil.which("sherlock")
|
|
157
|
+
if sherlock_exe is None:
|
|
158
|
+
raise FileNotFoundError(
|
|
159
|
+
"sherlock-proxyhat: the `sherlock` executable was not found on PATH. "
|
|
160
|
+
"Install it with `pip install sherlock-proxyhat[sherlock]` or `pip install sherlock-project`."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if _has_user_proxy(passthrough):
|
|
164
|
+
print(
|
|
165
|
+
"sherlock-proxyhat: a --proxy/-p flag was supplied explicitly; using it and skipping the ProxyHat proxy.",
|
|
166
|
+
file=sys.stderr,
|
|
167
|
+
)
|
|
168
|
+
return [sherlock_exe, *passthrough]
|
|
169
|
+
|
|
170
|
+
url = proxyhat_proxy_url(
|
|
171
|
+
api_key=ns.proxyhat_api_key,
|
|
172
|
+
username=ns.proxyhat_username,
|
|
173
|
+
password=ns.proxyhat_password,
|
|
174
|
+
sub_user=ns.proxyhat_sub_user,
|
|
175
|
+
country=ns.proxyhat_country,
|
|
176
|
+
region=ns.proxyhat_region,
|
|
177
|
+
city=ns.proxyhat_city,
|
|
178
|
+
sticky=_resolve_sticky(sticky=ns.proxyhat_sticky, ttl=ns.proxyhat_sticky_ttl),
|
|
179
|
+
filter=ns.proxyhat_filter,
|
|
180
|
+
protocol=ns.proxyhat_protocol,
|
|
181
|
+
)
|
|
182
|
+
return [sherlock_exe, "--proxy", url, *passthrough]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
186
|
+
"""Entry point for the ``sherlock-proxyhat`` console script.
|
|
187
|
+
|
|
188
|
+
Builds the command and hands off to the real ``sherlock`` via subprocess,
|
|
189
|
+
returning its exit code.
|
|
190
|
+
"""
|
|
191
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
192
|
+
try:
|
|
193
|
+
command = build_command(argv)
|
|
194
|
+
except (ProxyHatConfigError, FileNotFoundError) as exc:
|
|
195
|
+
print(str(exc), file=sys.stderr)
|
|
196
|
+
return 2
|
|
197
|
+
|
|
198
|
+
completed = subprocess.run(command)
|
|
199
|
+
return completed.returncode
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__": # pragma: no cover
|
|
203
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Tests for the launcher wiring.
|
|
2
|
+
|
|
3
|
+
Sherlock is a separate executable, so instead of installing it we fake
|
|
4
|
+
``shutil.which`` (to pretend ``sherlock`` is on PATH) and ``subprocess.run`` (to
|
|
5
|
+
capture the exact command line without running anything). Credentials are passed
|
|
6
|
+
explicitly so no network or SDK call is made. These assert the built ``--proxy``
|
|
7
|
+
URL reflects geo/rotation, that ``--proxyhat-*`` flags are stripped, and that all
|
|
8
|
+
other args reach ``sherlock`` untouched.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from sherlock_proxyhat import build_command, cli, main
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def fake_sherlock(monkeypatch):
|
|
20
|
+
"""Pretend `sherlock` is installed at a fixed path and capture subprocess.run."""
|
|
21
|
+
captured = {}
|
|
22
|
+
|
|
23
|
+
def fake_run(command, *args, **kwargs):
|
|
24
|
+
captured["command"] = command
|
|
25
|
+
|
|
26
|
+
class Completed:
|
|
27
|
+
returncode = 0
|
|
28
|
+
|
|
29
|
+
return Completed()
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(cli.shutil, "which", lambda name: "/usr/bin/sherlock" if name == "sherlock" else None)
|
|
32
|
+
monkeypatch.setattr(cli.subprocess, "run", fake_run)
|
|
33
|
+
return captured
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
CREDS = ["--proxyhat-username", "ph-1", "--proxyhat-password", "pw"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def fake_which(monkeypatch):
|
|
41
|
+
"""Pretend `sherlock` is installed at a fixed path (build_command needs it on PATH)."""
|
|
42
|
+
monkeypatch.setattr(cli.shutil, "which", lambda name: "/usr/bin/sherlock" if name == "sherlock" else None)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.usefixtures("fake_which")
|
|
46
|
+
class TestBuildCommand:
|
|
47
|
+
def test_injects_proxy_and_passes_through_args(self):
|
|
48
|
+
cmd = build_command([*CREDS, "johndoe", "janedoe", "--timeout", "30"])
|
|
49
|
+
assert cmd[0].endswith("sherlock")
|
|
50
|
+
assert cmd[1] == "--proxy"
|
|
51
|
+
url = cmd[2]
|
|
52
|
+
assert url.startswith("http://ph-1-country-any:pw@gate.proxyhat.com:8080")
|
|
53
|
+
# Usernames and sherlock's own flags pass through in order, proxyhat flags stripped.
|
|
54
|
+
assert cmd[3:] == ["johndoe", "janedoe", "--timeout", "30"]
|
|
55
|
+
assert not any(a.startswith("--proxyhat-") for a in cmd)
|
|
56
|
+
|
|
57
|
+
def test_rotating_by_default(self):
|
|
58
|
+
cmd = build_command([*CREDS, "johndoe"])
|
|
59
|
+
assert "-sid-" not in cmd[2] and "-ttl-" not in cmd[2]
|
|
60
|
+
|
|
61
|
+
def test_geo_and_sticky_ttl_reflected_in_url(self):
|
|
62
|
+
cmd = build_command([*CREDS, "--proxyhat-country", "de", "--proxyhat-sticky-ttl", "2h", "johndoe"])
|
|
63
|
+
url = cmd[2]
|
|
64
|
+
assert "ph-1-country-de" in url
|
|
65
|
+
assert "-ttl-2h" in url
|
|
66
|
+
assert cmd[3:] == ["johndoe"]
|
|
67
|
+
|
|
68
|
+
def test_sticky_bare_flag_pins_ip_without_eating_username(self):
|
|
69
|
+
# The sticky switch must not swallow the following positional username.
|
|
70
|
+
cmd = build_command([*CREDS, "--proxyhat-sticky", "johndoe"])
|
|
71
|
+
assert "-sid-" in cmd[2] and "-ttl-30m" in cmd[2]
|
|
72
|
+
assert cmd[3:] == ["johndoe"]
|
|
73
|
+
|
|
74
|
+
def test_socks5_protocol(self):
|
|
75
|
+
cmd = build_command([*CREDS, "--proxyhat-protocol", "socks5", "johndoe"])
|
|
76
|
+
assert cmd[2].startswith("socks5://ph-1-country-any:pw@gate.proxyhat.com:1080")
|
|
77
|
+
|
|
78
|
+
def test_user_supplied_proxy_wins(self, capsys):
|
|
79
|
+
cmd = build_command([*CREDS, "johndoe", "--proxy", "socks5://127.0.0.1:9050"])
|
|
80
|
+
# No ProxyHat proxy injected; the user's own args are forwarded verbatim.
|
|
81
|
+
assert cmd[1:] == ["johndoe", "--proxy", "socks5://127.0.0.1:9050"]
|
|
82
|
+
assert "skipping the ProxyHat proxy" in capsys.readouterr().err
|
|
83
|
+
|
|
84
|
+
def test_missing_sherlock_raises(self, monkeypatch):
|
|
85
|
+
monkeypatch.setattr(cli.shutil, "which", lambda name: None)
|
|
86
|
+
with pytest.raises(FileNotFoundError):
|
|
87
|
+
build_command([*CREDS, "johndoe"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestMain:
|
|
91
|
+
def test_main_runs_sherlock_and_returns_code(self, fake_sherlock):
|
|
92
|
+
rc = main([*CREDS, "--proxyhat-country", "us", "johndoe"])
|
|
93
|
+
assert rc == 0
|
|
94
|
+
command = fake_sherlock["command"]
|
|
95
|
+
assert command[0] == "/usr/bin/sherlock"
|
|
96
|
+
assert command[1] == "--proxy"
|
|
97
|
+
assert command[2].startswith("http://ph-1-country-us:pw@")
|
|
98
|
+
assert command[3] == "johndoe"
|
|
99
|
+
|
|
100
|
+
def test_main_missing_sherlock_returns_2(self, monkeypatch):
|
|
101
|
+
monkeypatch.setattr(cli.shutil, "which", lambda name: None)
|
|
102
|
+
assert main([*CREDS, "johndoe"]) == 2
|
|
103
|
+
|
|
104
|
+
def test_main_bad_credentials_returns_2(self, fake_sherlock, monkeypatch):
|
|
105
|
+
for var in ("PROXYHAT_API_KEY", "PROXYHAT_USERNAME", "PROXYHAT_PASSWORD", "PROXYHAT_SUBUSER"):
|
|
106
|
+
monkeypatch.delenv(var, raising=False)
|
|
107
|
+
assert main(["johndoe"]) == 2
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from sherlock_proxyhat import ProxyHatConfigError, proxyhat_proxy_url, resolve_credentials
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sub_user(**kw):
|
|
9
|
+
base = dict(
|
|
10
|
+
uuid="u",
|
|
11
|
+
name=None,
|
|
12
|
+
proxy_username="ph-1",
|
|
13
|
+
proxy_password="pw",
|
|
14
|
+
traffic_limit=0,
|
|
15
|
+
used_traffic=0,
|
|
16
|
+
suspended_at=None,
|
|
17
|
+
)
|
|
18
|
+
base.update(kw)
|
|
19
|
+
return SimpleNamespace(**base)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestProxyUrl:
|
|
23
|
+
def test_url_shape_and_geo(self):
|
|
24
|
+
url = proxyhat_proxy_url(username="ph-1", password="pw", country="us")
|
|
25
|
+
assert url.startswith("http://ph-1-country-us:pw@gate.proxyhat.com:8080")
|
|
26
|
+
|
|
27
|
+
def test_rotating_by_default(self):
|
|
28
|
+
# Sherlock reuses one proxy for the whole run, so the default rotates:
|
|
29
|
+
# no session id / ttl means the gateway hands out a fresh IP per connection.
|
|
30
|
+
url = proxyhat_proxy_url(username="ph-1", password="pw")
|
|
31
|
+
assert "-sid-" not in url
|
|
32
|
+
assert "-ttl-" not in url
|
|
33
|
+
assert url.startswith("http://ph-1-country-any:pw@")
|
|
34
|
+
|
|
35
|
+
def test_sticky_opt_in_pins_ip(self):
|
|
36
|
+
url = proxyhat_proxy_url(username="ph-1", password="pw", sticky=True)
|
|
37
|
+
assert "-sid-" in url
|
|
38
|
+
assert "-ttl-30m" in url
|
|
39
|
+
|
|
40
|
+
def test_custom_sticky_ttl(self):
|
|
41
|
+
url = proxyhat_proxy_url(username="ph-1", password="pw", sticky="2h")
|
|
42
|
+
assert "-ttl-2h" in url
|
|
43
|
+
|
|
44
|
+
def test_full_geo_targeting(self):
|
|
45
|
+
url = proxyhat_proxy_url(
|
|
46
|
+
username="ph-1",
|
|
47
|
+
password="pw",
|
|
48
|
+
country="de",
|
|
49
|
+
region="berlin",
|
|
50
|
+
city="berlin",
|
|
51
|
+
filter="high",
|
|
52
|
+
)
|
|
53
|
+
assert "ph-1-country-de" in url
|
|
54
|
+
assert "-region-berlin" in url
|
|
55
|
+
assert "-city-berlin" in url
|
|
56
|
+
assert "-filter-high" in url
|
|
57
|
+
|
|
58
|
+
def test_socks5_protocol(self):
|
|
59
|
+
url = proxyhat_proxy_url(username="ph-1", password="pw", protocol="socks5")
|
|
60
|
+
assert url.startswith("socks5://ph-1-country-any:pw@gate.proxyhat.com:1080")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestCredentialResolution:
|
|
64
|
+
def test_explicit_username_password(self):
|
|
65
|
+
assert resolve_credentials(username="ph-1", password="pw") == ("ph-1", "pw")
|
|
66
|
+
|
|
67
|
+
def test_raises_without_credentials(self, monkeypatch):
|
|
68
|
+
for var in ("PROXYHAT_API_KEY", "PROXYHAT_USERNAME", "PROXYHAT_PASSWORD", "PROXYHAT_SUBUSER"):
|
|
69
|
+
monkeypatch.delenv(var, raising=False)
|
|
70
|
+
with pytest.raises(ProxyHatConfigError):
|
|
71
|
+
resolve_credentials()
|
|
72
|
+
|
|
73
|
+
def test_api_key_picks_active_sub_user(self, monkeypatch):
|
|
74
|
+
users = [
|
|
75
|
+
sub_user(uuid="s", proxy_username="susp", suspended_at="2026-01-01"),
|
|
76
|
+
sub_user(uuid="g", proxy_username="good", traffic_limit=100, used_traffic=100),
|
|
77
|
+
sub_user(uuid="ok", proxy_username="ok", traffic_limit=100, used_traffic=1),
|
|
78
|
+
]
|
|
79
|
+
fake_client = SimpleNamespace(sub_users=SimpleNamespace(list=lambda: users))
|
|
80
|
+
monkeypatch.setattr("sherlock_proxyhat._resolve.ProxyHat", lambda **kw: fake_client)
|
|
81
|
+
assert resolve_credentials(api_key="ph_key") == ("ok", "pw")
|
|
82
|
+
|
|
83
|
+
def test_api_key_named_sub_user(self, monkeypatch):
|
|
84
|
+
users = [sub_user(uuid="a", proxy_username="a"), sub_user(uuid="b", name="prod", proxy_username="b")]
|
|
85
|
+
fake_client = SimpleNamespace(sub_users=SimpleNamespace(list=lambda: users))
|
|
86
|
+
monkeypatch.setattr("sherlock_proxyhat._resolve.ProxyHat", lambda **kw: fake_client)
|
|
87
|
+
assert resolve_credentials(api_key="ph_key", sub_user="prod") == ("b", "pw")
|
|
88
|
+
|
|
89
|
+
def test_api_key_no_usable_sub_user(self, monkeypatch):
|
|
90
|
+
users = [sub_user(traffic_limit=100, used_traffic=100)]
|
|
91
|
+
fake_client = SimpleNamespace(sub_users=SimpleNamespace(list=lambda: users))
|
|
92
|
+
monkeypatch.setattr("sherlock_proxyhat._resolve.ProxyHat", lambda **kw: fake_client)
|
|
93
|
+
with pytest.raises(ProxyHatConfigError):
|
|
94
|
+
resolve_credentials(api_key="ph_key")
|
|
95
|
+
|
|
96
|
+
def test_url_resolves_via_api_key(self, monkeypatch):
|
|
97
|
+
users = [sub_user(proxy_username="good", proxy_password="secret")]
|
|
98
|
+
fake_client = SimpleNamespace(sub_users=SimpleNamespace(list=lambda: users))
|
|
99
|
+
monkeypatch.setattr("sherlock_proxyhat._resolve.ProxyHat", lambda **kw: fake_client)
|
|
100
|
+
url = proxyhat_proxy_url(api_key="ph_key", country="us")
|
|
101
|
+
assert url.startswith("http://good-country-us:secret@gate.proxyhat.com:8080")
|