protox-gatekeeper 0.1.1__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.
- protox_gatekeeper-0.1.1/LICENSE +21 -0
- protox_gatekeeper-0.1.1/PKG-INFO +207 -0
- protox_gatekeeper-0.1.1/README.md +173 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper/__init__.py +3 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper/core.py +92 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper/geo.py +26 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper/ops.py +26 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper/session.py +11 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper/verify.py +14 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper.egg-info/PKG-INFO +207 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper.egg-info/SOURCES.txt +14 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper.egg-info/dependency_links.txt +1 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper.egg-info/requires.txt +2 -0
- protox_gatekeeper-0.1.1/protox_gatekeeper.egg-info/top_level.txt +1 -0
- protox_gatekeeper-0.1.1/pyproject.toml +19 -0
- protox_gatekeeper-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Erik Harnes
|
|
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,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: protox-gatekeeper
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fail-closed Tor session enforcement for Python HTTP(S) traffic
|
|
5
|
+
Author: Tom Erik Harnes
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Tom Erik Harnes
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: pysocks
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# ProtoX GateKeeper
|
|
36
|
+
|
|
37
|
+
**ProtoX GateKeeper** is a small, opinionated Python library that enforces
|
|
38
|
+
**fail‑closed Tor routing** for HTTP(S) traffic.
|
|
39
|
+
|
|
40
|
+
The goal is simple:
|
|
41
|
+
|
|
42
|
+
> If Tor is not active and verified, **nothing runs**.
|
|
43
|
+
|
|
44
|
+
GateKeeper is designed to be *fire‑and‑forget*: create a client once, then perform network operations with a hard guarantee that traffic exits through the Tor network.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## What GateKeeper Is
|
|
49
|
+
|
|
50
|
+
- A **Tor‑verified HTTP client**
|
|
51
|
+
- A thin wrapper around `requests.Session`
|
|
52
|
+
- Fail‑closed by default (no silent clearnet fallback)
|
|
53
|
+
- Observable (exit IP, optional geo info)
|
|
54
|
+
- Suitable for scripts, tooling, and automation
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## What GateKeeper Is NOT
|
|
59
|
+
|
|
60
|
+
- ❌ A Tor controller
|
|
61
|
+
- ❌ A crawler or scanner
|
|
62
|
+
- ❌ An anonymization silver bullet
|
|
63
|
+
- ❌ A replacement for Tor Browser
|
|
64
|
+
|
|
65
|
+
GateKeeper enforces transport routing only. You are still responsible for *what* you do with it.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- A locally running Tor client
|
|
72
|
+
- SOCKS proxy enabled (default: `127.0.0.1:9150`)
|
|
73
|
+
|
|
74
|
+
On Windows this usually means **Tor Browser** running in the background.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
### From source (development)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
(Recommended while developing or testing.)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Basic Usage
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import logging
|
|
94
|
+
from protox_gatekeeper import GateKeeper
|
|
95
|
+
|
|
96
|
+
logging.basicConfig(
|
|
97
|
+
level=logging.INFO,
|
|
98
|
+
format='[%(levelname)s] %(name)s - %(message)s'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
gk = GateKeeper(geo=True)
|
|
102
|
+
|
|
103
|
+
gk.download(
|
|
104
|
+
"https://httpbin.org/bytes/1024",
|
|
105
|
+
"downloads/test.bin"
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Example output
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
[INFO] gatekeeper.core - Tor verified: 89.xxx.xxx.xxx -> 185.xxx.xxx.xxx
|
|
113
|
+
[INFO] gatekeeper.core - Tor exit location: Brandenburg, DE
|
|
114
|
+
[INFO] gatekeeper.core - [Tor 185.xxx.xxx.xxx] downloading https://httpbin.org/bytes/1024 -> downloads/test.bin
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This confirms:
|
|
118
|
+
- clearnet IP was measured
|
|
119
|
+
- Tor routing was verified
|
|
120
|
+
- all traffic used the Tor exit shown
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## API Overview
|
|
125
|
+
|
|
126
|
+
### `GateKeeper(...)`
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
gk = GateKeeper(
|
|
130
|
+
socks_port=9150,
|
|
131
|
+
geo=False
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Parameters**:
|
|
136
|
+
- `socks_port` *(int)* – Tor SOCKS port (default: `9150`)
|
|
137
|
+
- `geo` *(bool)* – Enable best‑effort Tor exit geolocation (optional)
|
|
138
|
+
|
|
139
|
+
Raises `RuntimeError` if Tor routing cannot be verified.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `download(url, target_path)`
|
|
144
|
+
|
|
145
|
+
Downloads a resource **through the verified Tor session**.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
gk.download(url, target_path)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `url` – HTTP(S) URL
|
|
152
|
+
- `target_path` – Full local file path (directories created automatically)
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Design Principles
|
|
157
|
+
|
|
158
|
+
- **Fail closed**: no Tor → no execution
|
|
159
|
+
- **Single verification point** (during construction)
|
|
160
|
+
- **No global state**
|
|
161
|
+
- **No logging configuration inside the library**
|
|
162
|
+
- **Session reuse without re‑verification**
|
|
163
|
+
|
|
164
|
+
Logging is emitted by the library, but **configured by the application**.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Logging
|
|
169
|
+
|
|
170
|
+
GateKeeper uses standard Python logging:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import logging
|
|
174
|
+
logging.basicConfig(level=logging.INFO)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The library does **not** call `logging.basicConfig()` internally.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Security Notes
|
|
182
|
+
|
|
183
|
+
- Tor exit IPs may rotate over time
|
|
184
|
+
- Geo information is best‑effort and may be unavailable (rate‑limits, CAPTCHAs)
|
|
185
|
+
- GateKeeper guarantees routing, not anonymity
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT License
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Status
|
|
196
|
+
|
|
197
|
+
- Version: **v0.1.1**
|
|
198
|
+
- Phase 1 complete
|
|
199
|
+
- API intentionally minimal
|
|
200
|
+
|
|
201
|
+
Future versions may add optional features such as:
|
|
202
|
+
- circuit rotation
|
|
203
|
+
- ControlPort support
|
|
204
|
+
- higher‑level request helpers
|
|
205
|
+
|
|
206
|
+
Without breaking the core contract.
|
|
207
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# ProtoX GateKeeper
|
|
2
|
+
|
|
3
|
+
**ProtoX GateKeeper** is a small, opinionated Python library that enforces
|
|
4
|
+
**fail‑closed Tor routing** for HTTP(S) traffic.
|
|
5
|
+
|
|
6
|
+
The goal is simple:
|
|
7
|
+
|
|
8
|
+
> If Tor is not active and verified, **nothing runs**.
|
|
9
|
+
|
|
10
|
+
GateKeeper is designed to be *fire‑and‑forget*: create a client once, then perform network operations with a hard guarantee that traffic exits through the Tor network.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What GateKeeper Is
|
|
15
|
+
|
|
16
|
+
- A **Tor‑verified HTTP client**
|
|
17
|
+
- A thin wrapper around `requests.Session`
|
|
18
|
+
- Fail‑closed by default (no silent clearnet fallback)
|
|
19
|
+
- Observable (exit IP, optional geo info)
|
|
20
|
+
- Suitable for scripts, tooling, and automation
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What GateKeeper Is NOT
|
|
25
|
+
|
|
26
|
+
- ❌ A Tor controller
|
|
27
|
+
- ❌ A crawler or scanner
|
|
28
|
+
- ❌ An anonymization silver bullet
|
|
29
|
+
- ❌ A replacement for Tor Browser
|
|
30
|
+
|
|
31
|
+
GateKeeper enforces transport routing only. You are still responsible for *what* you do with it.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- A locally running Tor client
|
|
38
|
+
- SOCKS proxy enabled (default: `127.0.0.1:9150`)
|
|
39
|
+
|
|
40
|
+
On Windows this usually means **Tor Browser** running in the background.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
### From source (development)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
(Recommended while developing or testing.)
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Basic Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import logging
|
|
60
|
+
from protox_gatekeeper import GateKeeper
|
|
61
|
+
|
|
62
|
+
logging.basicConfig(
|
|
63
|
+
level=logging.INFO,
|
|
64
|
+
format='[%(levelname)s] %(name)s - %(message)s'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
gk = GateKeeper(geo=True)
|
|
68
|
+
|
|
69
|
+
gk.download(
|
|
70
|
+
"https://httpbin.org/bytes/1024",
|
|
71
|
+
"downloads/test.bin"
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Example output
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
[INFO] gatekeeper.core - Tor verified: 89.xxx.xxx.xxx -> 185.xxx.xxx.xxx
|
|
79
|
+
[INFO] gatekeeper.core - Tor exit location: Brandenburg, DE
|
|
80
|
+
[INFO] gatekeeper.core - [Tor 185.xxx.xxx.xxx] downloading https://httpbin.org/bytes/1024 -> downloads/test.bin
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This confirms:
|
|
84
|
+
- clearnet IP was measured
|
|
85
|
+
- Tor routing was verified
|
|
86
|
+
- all traffic used the Tor exit shown
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## API Overview
|
|
91
|
+
|
|
92
|
+
### `GateKeeper(...)`
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
gk = GateKeeper(
|
|
96
|
+
socks_port=9150,
|
|
97
|
+
geo=False
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Parameters**:
|
|
102
|
+
- `socks_port` *(int)* – Tor SOCKS port (default: `9150`)
|
|
103
|
+
- `geo` *(bool)* – Enable best‑effort Tor exit geolocation (optional)
|
|
104
|
+
|
|
105
|
+
Raises `RuntimeError` if Tor routing cannot be verified.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### `download(url, target_path)`
|
|
110
|
+
|
|
111
|
+
Downloads a resource **through the verified Tor session**.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
gk.download(url, target_path)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `url` – HTTP(S) URL
|
|
118
|
+
- `target_path` – Full local file path (directories created automatically)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Design Principles
|
|
123
|
+
|
|
124
|
+
- **Fail closed**: no Tor → no execution
|
|
125
|
+
- **Single verification point** (during construction)
|
|
126
|
+
- **No global state**
|
|
127
|
+
- **No logging configuration inside the library**
|
|
128
|
+
- **Session reuse without re‑verification**
|
|
129
|
+
|
|
130
|
+
Logging is emitted by the library, but **configured by the application**.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Logging
|
|
135
|
+
|
|
136
|
+
GateKeeper uses standard Python logging:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import logging
|
|
140
|
+
logging.basicConfig(level=logging.INFO)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The library does **not** call `logging.basicConfig()` internally.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Security Notes
|
|
148
|
+
|
|
149
|
+
- Tor exit IPs may rotate over time
|
|
150
|
+
- Geo information is best‑effort and may be unavailable (rate‑limits, CAPTCHAs)
|
|
151
|
+
- GateKeeper guarantees routing, not anonymity
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT License
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Status
|
|
162
|
+
|
|
163
|
+
- Version: **v0.1.1**
|
|
164
|
+
- Phase 1 complete
|
|
165
|
+
- API intentionally minimal
|
|
166
|
+
|
|
167
|
+
Future versions may add optional features such as:
|
|
168
|
+
- circuit rotation
|
|
169
|
+
- ControlPort support
|
|
170
|
+
- higher‑level request helpers
|
|
171
|
+
|
|
172
|
+
Without breaking the core contract.
|
|
173
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from protox_gatekeeper.session import make_tor_session
|
|
6
|
+
from protox_gatekeeper.verify import is_tor_exit, get_public_ip
|
|
7
|
+
from protox_gatekeeper.ops import download_file as _download
|
|
8
|
+
from protox_gatekeeper.geo import geo_lookup
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GateKeeper:
|
|
14
|
+
def __init__(self, socks_port: int = 9150, geo=False, timeout: int = 10):
|
|
15
|
+
"""
|
|
16
|
+
GateKeeper constructor.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
socks_port (int, optional): The socks port to use. Defaults to 9150.
|
|
20
|
+
geo (bool, optional): Whether to use geo. Defaults to False.
|
|
21
|
+
timeout (int, optional): The timeout to wait for a response. Defaults to 10.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
self._session: requests.Session
|
|
25
|
+
self.exit_ip: str
|
|
26
|
+
self.clearnet_ip: str
|
|
27
|
+
|
|
28
|
+
# 1) Measure clearnet IP (no proxies)
|
|
29
|
+
clearnet = requests.Session()
|
|
30
|
+
self.clearnet_ip = get_public_ip(session=clearnet, timeout=timeout)
|
|
31
|
+
|
|
32
|
+
# 2) Create Tor session
|
|
33
|
+
self._session = make_tor_session(port=socks_port)
|
|
34
|
+
|
|
35
|
+
# 3) Verify Tor routing
|
|
36
|
+
if not is_tor_exit(session=self._session, timeout=timeout):
|
|
37
|
+
raise RuntimeError('Tor verification failed. Execution aborted.')
|
|
38
|
+
|
|
39
|
+
# 4) Measure Tor exit IP
|
|
40
|
+
self.exit_ip = get_public_ip(session=self._session, timeout=timeout)
|
|
41
|
+
|
|
42
|
+
# 5) Log the transition
|
|
43
|
+
logger.info(f'Tor verified: {self.clearnet_ip} -> {self.exit_ip}')
|
|
44
|
+
|
|
45
|
+
# 6) Location data
|
|
46
|
+
if geo:
|
|
47
|
+
location = geo_lookup(self.exit_ip)
|
|
48
|
+
if location:
|
|
49
|
+
logger.info(f'Tor exit location: {location}')
|
|
50
|
+
else:
|
|
51
|
+
logger.info('Tor exit location: Unavailable')
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return f'<GateKeeper: {self.clearnet_ip} -> tor_exit: {self.exit_ip}>'
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> "GateKeeper":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, exc_type, exc, tb):
|
|
60
|
+
self._session.close()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def session(self) -> requests.Session:
|
|
64
|
+
""" Exposes the verified session if needed. """
|
|
65
|
+
return self._session
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def tor_exit(self) -> str:
|
|
69
|
+
""" Returns the Tor exit IP address. """
|
|
70
|
+
return self.exit_ip
|
|
71
|
+
|
|
72
|
+
def download(self, url: str, target_path: str, timeout: int = 30,
|
|
73
|
+
chunk_size: int = 8192):
|
|
74
|
+
"""
|
|
75
|
+
Attempts to download the given url to the target path.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
url (str): The url to download.
|
|
79
|
+
target_path (str): The target path.
|
|
80
|
+
timeout (int, optional): The timeout to wait for a response.
|
|
81
|
+
chunk_size (int, optional): The chunk size to use for download.
|
|
82
|
+
"""
|
|
83
|
+
logger.info(
|
|
84
|
+
f'[Tor {self.tor_exit}] downloading {url} -> {target_path}')
|
|
85
|
+
|
|
86
|
+
return _download(
|
|
87
|
+
session=self._session,
|
|
88
|
+
url=url,
|
|
89
|
+
target_path=target_path,
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
chunk_size=chunk_size
|
|
92
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def geo_lookup(ip: str) -> str | None:
|
|
9
|
+
try:
|
|
10
|
+
r = requests.get(
|
|
11
|
+
url=f'https://ipapi.co/{ip}/json',
|
|
12
|
+
timeout=10,
|
|
13
|
+
headers={'User-Agent': 'GateKeeper/0.1.0'}
|
|
14
|
+
)
|
|
15
|
+
if r.status_code != 200:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
data = r.json()
|
|
19
|
+
city = data.get('city')
|
|
20
|
+
country = data.get('country')
|
|
21
|
+
if city and country:
|
|
22
|
+
return f'{city}, {country}'
|
|
23
|
+
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.info(f'Unable to get geolocation for {ip}: {e}')
|
|
26
|
+
return None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def download_file(
|
|
7
|
+
session: requests.Session,
|
|
8
|
+
url: str,
|
|
9
|
+
target_path: str,
|
|
10
|
+
timeout: int,
|
|
11
|
+
chunk_size: int
|
|
12
|
+
) -> None:
|
|
13
|
+
if not isinstance(session, requests.Session):
|
|
14
|
+
raise TypeError('A verified requests.Session is required.')
|
|
15
|
+
|
|
16
|
+
dir_path = os.path.dirname(target_path)
|
|
17
|
+
if dir_path:
|
|
18
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
response = session.get(url, stream=True, timeout=timeout)
|
|
21
|
+
response.raise_for_status()
|
|
22
|
+
|
|
23
|
+
with open(target_path, 'wb') as f:
|
|
24
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
25
|
+
if chunk:
|
|
26
|
+
f.write(chunk)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_public_ip(session: requests.Session, timeout: int) -> str:
|
|
5
|
+
r = session.get(url='https://api.ipify.org/', timeout=timeout)
|
|
6
|
+
r.raise_for_status()
|
|
7
|
+
return r.text.strip()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_tor_exit(session: requests.Session, timeout: int) -> bool:
|
|
11
|
+
r = session.get(url='https://check.torproject.org/api/ip', timeout=timeout)
|
|
12
|
+
r.raise_for_status()
|
|
13
|
+
data = r.json()
|
|
14
|
+
return bool(data.get('IsTor', False))
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: protox-gatekeeper
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fail-closed Tor session enforcement for Python HTTP(S) traffic
|
|
5
|
+
Author: Tom Erik Harnes
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Tom Erik Harnes
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: pysocks
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# ProtoX GateKeeper
|
|
36
|
+
|
|
37
|
+
**ProtoX GateKeeper** is a small, opinionated Python library that enforces
|
|
38
|
+
**fail‑closed Tor routing** for HTTP(S) traffic.
|
|
39
|
+
|
|
40
|
+
The goal is simple:
|
|
41
|
+
|
|
42
|
+
> If Tor is not active and verified, **nothing runs**.
|
|
43
|
+
|
|
44
|
+
GateKeeper is designed to be *fire‑and‑forget*: create a client once, then perform network operations with a hard guarantee that traffic exits through the Tor network.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## What GateKeeper Is
|
|
49
|
+
|
|
50
|
+
- A **Tor‑verified HTTP client**
|
|
51
|
+
- A thin wrapper around `requests.Session`
|
|
52
|
+
- Fail‑closed by default (no silent clearnet fallback)
|
|
53
|
+
- Observable (exit IP, optional geo info)
|
|
54
|
+
- Suitable for scripts, tooling, and automation
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## What GateKeeper Is NOT
|
|
59
|
+
|
|
60
|
+
- ❌ A Tor controller
|
|
61
|
+
- ❌ A crawler or scanner
|
|
62
|
+
- ❌ An anonymization silver bullet
|
|
63
|
+
- ❌ A replacement for Tor Browser
|
|
64
|
+
|
|
65
|
+
GateKeeper enforces transport routing only. You are still responsible for *what* you do with it.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- A locally running Tor client
|
|
72
|
+
- SOCKS proxy enabled (default: `127.0.0.1:9150`)
|
|
73
|
+
|
|
74
|
+
On Windows this usually means **Tor Browser** running in the background.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
### From source (development)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
(Recommended while developing or testing.)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Basic Usage
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import logging
|
|
94
|
+
from protox_gatekeeper import GateKeeper
|
|
95
|
+
|
|
96
|
+
logging.basicConfig(
|
|
97
|
+
level=logging.INFO,
|
|
98
|
+
format='[%(levelname)s] %(name)s - %(message)s'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
gk = GateKeeper(geo=True)
|
|
102
|
+
|
|
103
|
+
gk.download(
|
|
104
|
+
"https://httpbin.org/bytes/1024",
|
|
105
|
+
"downloads/test.bin"
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Example output
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
[INFO] gatekeeper.core - Tor verified: 89.xxx.xxx.xxx -> 185.xxx.xxx.xxx
|
|
113
|
+
[INFO] gatekeeper.core - Tor exit location: Brandenburg, DE
|
|
114
|
+
[INFO] gatekeeper.core - [Tor 185.xxx.xxx.xxx] downloading https://httpbin.org/bytes/1024 -> downloads/test.bin
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This confirms:
|
|
118
|
+
- clearnet IP was measured
|
|
119
|
+
- Tor routing was verified
|
|
120
|
+
- all traffic used the Tor exit shown
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## API Overview
|
|
125
|
+
|
|
126
|
+
### `GateKeeper(...)`
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
gk = GateKeeper(
|
|
130
|
+
socks_port=9150,
|
|
131
|
+
geo=False
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Parameters**:
|
|
136
|
+
- `socks_port` *(int)* – Tor SOCKS port (default: `9150`)
|
|
137
|
+
- `geo` *(bool)* – Enable best‑effort Tor exit geolocation (optional)
|
|
138
|
+
|
|
139
|
+
Raises `RuntimeError` if Tor routing cannot be verified.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `download(url, target_path)`
|
|
144
|
+
|
|
145
|
+
Downloads a resource **through the verified Tor session**.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
gk.download(url, target_path)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `url` – HTTP(S) URL
|
|
152
|
+
- `target_path` – Full local file path (directories created automatically)
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Design Principles
|
|
157
|
+
|
|
158
|
+
- **Fail closed**: no Tor → no execution
|
|
159
|
+
- **Single verification point** (during construction)
|
|
160
|
+
- **No global state**
|
|
161
|
+
- **No logging configuration inside the library**
|
|
162
|
+
- **Session reuse without re‑verification**
|
|
163
|
+
|
|
164
|
+
Logging is emitted by the library, but **configured by the application**.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Logging
|
|
169
|
+
|
|
170
|
+
GateKeeper uses standard Python logging:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import logging
|
|
174
|
+
logging.basicConfig(level=logging.INFO)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The library does **not** call `logging.basicConfig()` internally.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Security Notes
|
|
182
|
+
|
|
183
|
+
- Tor exit IPs may rotate over time
|
|
184
|
+
- Geo information is best‑effort and may be unavailable (rate‑limits, CAPTCHAs)
|
|
185
|
+
- GateKeeper guarantees routing, not anonymity
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT License
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Status
|
|
196
|
+
|
|
197
|
+
- Version: **v0.1.1**
|
|
198
|
+
- Phase 1 complete
|
|
199
|
+
- API intentionally minimal
|
|
200
|
+
|
|
201
|
+
Future versions may add optional features such as:
|
|
202
|
+
- circuit rotation
|
|
203
|
+
- ControlPort support
|
|
204
|
+
- higher‑level request helpers
|
|
205
|
+
|
|
206
|
+
Without breaking the core contract.
|
|
207
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
protox_gatekeeper/__init__.py
|
|
5
|
+
protox_gatekeeper/core.py
|
|
6
|
+
protox_gatekeeper/geo.py
|
|
7
|
+
protox_gatekeeper/ops.py
|
|
8
|
+
protox_gatekeeper/session.py
|
|
9
|
+
protox_gatekeeper/verify.py
|
|
10
|
+
protox_gatekeeper.egg-info/PKG-INFO
|
|
11
|
+
protox_gatekeeper.egg-info/SOURCES.txt
|
|
12
|
+
protox_gatekeeper.egg-info/dependency_links.txt
|
|
13
|
+
protox_gatekeeper.egg-info/requires.txt
|
|
14
|
+
protox_gatekeeper.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
protox_gatekeeper
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "protox-gatekeeper"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Fail-closed Tor session enforcement for Python HTTP(S) traffic"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests",
|
|
14
|
+
"pysocks"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
authors = [
|
|
18
|
+
{ name = "Tom Erik Harnes" }
|
|
19
|
+
]
|