Metadata-Version: 2.4
Name: tor-native-requests
Version: 0.2.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: System :: Networking
Classifier: Topic :: Internet
Classifier: Topic :: Security
Requires-Dist: pytest>=7.0 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21 ; extra == 'dev'
Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
Requires-Dist: pytest-timeout>=2.0 ; extra == 'dev'
Requires-Dist: requests>=2.28 ; extra == 'dev'
Requires-Dist: black>=23.0 ; extra == 'dev'
Requires-Dist: mypy>=1.0 ; extra == 'dev'
Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
Requires-Dist: requests>=2.28 ; extra == 'requests'
Provides-Extra: dev
Provides-Extra: requests
License-File: LICENSE
Summary: Drop-in socket replacement for transparent Tor routing in Python
Keywords: tor,anonymity,socket,networking,proxy,onion,privacy
Author-email: Python Developer <pypi.org@cri.xyz>
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Issues, https://github.com/bshuler/tor-native-requests/issues
Project-URL: Repository, https://github.com/bshuler/tor-native-requests

# tor-native-requests

Drop-in socket replacement for transparent Tor routing in Python.

Routes TCP traffic through the [Tor network](https://www.torproject.org/) using [arti-client](https://gitlab.torproject.org/tpo/core/arti) (the Tor Project's pure-Rust Tor implementation). No need to install the Tor daemon — everything is self-contained in a native Python extension.

## Installation

```bash
pip install tor-native-requests
```

For `requests` library integration:

```bash
pip install tor-native-requests[requests]
```

## Quick Start

### Context Manager (recommended)

Route all TCP traffic through Tor within a context:

```python
import requests
from tor_native_requests import tor_context

with tor_context():
    r = requests.get("https://check.torproject.org/api/ip")
    print(r.json())  # {"IP": "...", "IsTor": true}
```

### Requests Session

Create a `requests.Session` that routes through Tor:

```python
from tor_native_requests import create_session

session = create_session()
resp = session.get("https://httpbin.org/ip")
print(resp.json())
session.close()
```

### Raw Socket

Use `TorSocket` as a drop-in `socket.socket` replacement:

```python
from tor_native_requests import TorConfig, TorSocket
from tor_native_requests._native import TorTunnel

config = TorConfig()
tunnel = TorTunnel(config.to_native())

sock = TorSocket(tunnel)
sock.connect(("httpbin.org", 80))
sock.sendall(b"GET /ip HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n")
print(sock.recv(4096))
sock.close()
tunnel.close()
```

## .onion Services

Access Tor hidden services directly:

```python
from tor_native_requests import create_session

session = create_session()
resp = session.get("https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/")
print(resp.status_code)
session.close()
```

## Circuit Isolation

Use separate Tor circuits per connection for different exit IPs:

```python
from tor_native_requests import TorConfig, create_session

config = TorConfig(isolation=True)
session = create_session(config)
# Each request may use a different exit node
resp = session.get("https://check.torproject.org/api/ip")
print(resp.json()["IP"])
session.close()
```

## Bridge Relays

> **Not yet supported.** Passing `bridges` to `TorConfig` currently raises a `ValueError`. This section documents the intended API and bridge line format for when support lands. Track progress in the issue tracker.

Bridge relays help reach Tor on censored networks. The `bridges` list accepts standard bridge lines in the same format used by Tor Browser — one string per bridge.

### Vanilla bridge (direct TCP, no obfuscation)

```python
from tor_native_requests import TorConfig, create_session

# A vanilla bridge is just an IP:port plus the relay fingerprint.
# Format: <ip>:<port> <fingerprint>
config = TorConfig(bridges=[
    "192.0.2.47:9001 A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2",
])
session = create_session(config)
```

### obfs4 bridge (recommended for censored networks)

```python
# obfs4 lines include the pluggable-transport name, address, fingerprint,
# the base64 certificate, and the IAT (inter-arrival timing) mode.
# Format: obfs4 <ip>:<port> <fingerprint> cert=<base64> iat-mode=<0|1|2>
config = TorConfig(bridges=[
    "obfs4 198.51.100.73:443 B3C4D5E6F7A8B3C4D5E6F7A8B3C4D5E6F7A8B3C4 cert=bGV0bWVpbnRoZW5ldHdvcmtwbGVhc2V0aGFua3lvdXZlcnltdWNoaGVsbG8K iat-mode=0",
    "obfs4 203.0.113.18:8443 C4D5E6F7A8B9C4D5E6F7A8B9C4D5E6F7A8B9C4D5 cert=dGhpc2lzYWZha2VjZXJ0aWZpY2F0ZWZvcmRvY3VtZW50YXRpb25wdXJwb3Nlcwo= iat-mode=0",
])
session = create_session(config)
```

**Bridge line fields:**

| Field | Example | Notes |
|-------|---------|-------|
| Transport | `obfs4` | Omit for vanilla bridges |
| Address | `198.51.100.73:443` | RFC 5737 documentation range |
| Fingerprint | `B3C4D5E6F7A8B3C4` (40 hex chars) | SHA-1 of the relay's identity key |
| `cert=` | base64 string | obfs4 only — public key material |
| `iat-mode=` | `0` | obfs4 only — `0` disables timing obfuscation |

Obtain real bridge lines from [bridges.torproject.org](https://bridges.torproject.org/) or by emailing bridges@torproject.org.

## Async Support

```python
import asyncio
from tor_native_requests import TorConfig
from tor_native_requests._native import TorTunnel
from tor_native_requests.async_socket import AsyncTorSocket

async def main():
    config = TorConfig()
    tunnel = TorTunnel(config.to_native())

    async with AsyncTorSocket(tunnel) as sock:
        await sock.connect(("httpbin.org", 80))
        await sock.send(b"GET /ip HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n")
        data = await sock.recv(4096)
        print(data)

    tunnel.close()

asyncio.run(main())
```

## Configuration

`TorConfig` accepts the following options:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `data_dir` | `str \| None` | `None` | Directory for Tor state/cache (uses arti default) |
| `bridges` | `list[str]` | `[]` | Bridge relay lines for censored networks |
| `isolation` | `bool` | `False` | Per-stream circuit isolation for IP diversity |

## Building from Source

Requires Rust toolchain (1.70+) and Python 3.9+:

```bash
git clone https://github.com/bshuler/tor-native-requests
cd tor-native-requests
pip install maturin
maturin develop
```

Run tests:

```bash
pip install pytest pytest-asyncio
pytest tests/unit -v
```

## Architecture

- **Rust native extension** (`arti-client` + `tokio`): Handles Tor protocol, circuit building, and async I/O
- **Python wrapper**: Provides `socket.socket`-compatible API, TLS via `ssl.MemoryBIO`, and `requests` integration
- **Async-to-sync bridge**: Background tokio runtime with crossbeam channels for Python↔Rust communication

## License

Apache-2.0

