Skip to content

LouisCourrian/tornion

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧅 tornion

Tor hidden service toolkit for Python — drop-in client + zero-config server.

PyPI version Python versions License: MIT

Consume any .onion API, or publish your own — in 3 lines of Python.


from tornion import client, server

# Consume a .onion API
r = client.get("http://xxxxxxxxxxxxxx.onion/ping")

# Publish your own
from fastapi import FastAPI
app = FastAPI()
server.serve(app)

That's it. No Docker, no torrc, no manual tor install — tornion handles everything: discovers or downloads the tor binary, spawns it, manages the SOCKS proxy or the hidden service, and tears it down on exit.

✨ Features

  • 🔌 Drop-in requestsclient.Session is a real requests.Session subclass
  • 🚀 Zero-config serverserver.serve(app) takes any ASGI or WSGI app
  • 🧠 Smart tor reuse — auto-detects an already-running tor on :9050/:9150
  • 📦 Auto-install tor — downloads the official Tor Expert Bundle on first use
  • 🎯 Framework-agnostic — FastAPI, Flask, Starlette, Django, Quart, Litestar…
  • 🔑 Persistent .onion — the address stays stable across restarts
  • 🪶 Lightweight client — server features are opt-in via pip install tornion[server]

📦 Installation

# Client only
pip install tornion

# With server features
pip install tornion[server]

🚀 Quick start

Client — call a .onion API

from tornion import client

r = client.get("http://xxx.onion/ping")
print(r.json())

# Reusable session for multiple calls
with client.Session() as s:
    s.post("http://xxx.onion/items", json={"name": "foo"})
    s.get("http://xxx.onion/items/1")

📖 Full client guide →

Server — publish your app on a .onion

from fastapi import FastAPI
from tornion import server

app = FastAPI()

@app.get("/")
def root():
    return {"hello": "from .onion"}

server.serve(app)

When you run this, tornion prints the .onion URL and blocks until Ctrl+C. Same code works with Flask, Starlette, Django, etc. — auto-detected.

📖 Full server guide →

Hybrid — server that also makes outbound calls

from fastapi import FastAPI
from tornion import client, server

app = FastAPI()

@app.get("/relay")
def relay(target: str):
    r = client.get(target, timeout=30)
    return {"upstream": r.status_code, "body": r.json()}

server.serve(app)

🛠️ CLI

tornion install-tor              # pre-download the tor binary
tornion serve myapp:app          # uvicorn-style: run an app on a .onion
tornion get http://xxx.onion/    # one-shot HTTP request
tornion info                     # diagnostic: where's tor, what's installed

📖 Configuration & CLI reference →

🔍 How does it actually work?

tornion orchestrates a real tor binary as a subprocess. Python doesn't implement Tor — it uses the C reference implementation (tor) under the hood, talks to it via SOCKS5 (client side) or the hidden-service API (server side), and shuts it down on exit.

📖 Architecture & internals →

📚 Documentation

Topic Doc
Calling .onion APIs from Python docs/client.md
Publishing your app on a hidden service docs/server.md
Env vars, CLI, paths, diagnostic docs/configuration.md
Architecture, design choices, pitfalls docs/internals.md
Examples (client / server / hybrid) examples/
Release history & versioning policy CHANGELOG.md

🧪 Try it

git clone https://github.com/LouisCourrian/tornion
cd tornion
pip install -e ".[dev]"
pytest
python examples/server_fastapi.py

✅ Status

tornion is stable as of 1.0.0. The public API of tornion, tornion.client, and tornion.server follows Semantic Versioning; see CHANGELOG.md for the full versioning policy and release history. Pin to a major version (tornion>=1.0,<2.0) and you're good.

🗺️ Roadmap

Required for 1.0.0 (✅ all shipped):

  • Stable .onion by default. Stop deriving app_name from app.title (fragile). app_name now defaults to the entry-script basename (python myserver.pymyserver); serve() prints the resolved key_dir and a fresh-vs-existing identity status before tor bootstrap so the first run is never silent.
  • Verify Tor Expert Bundle downloads. SHA-256 pinning. Hashes live in KNOWN_TOR_HASHES, populated from the Tor Project's signed sha256sums-signed-build.txt. Unknown versions are refused unless the caller passes an explicit sha256=.... Mismatched archives are deleted before extraction.
  • CHANGELOG.md + written SemVer policy. See CHANGELOG.md — Keep-a-Changelog format, with an explicit policy at the top stating what counts as MAJOR / MINOR / PATCH and the deprecation window.
  • Publish to PyPI. Auto-release workflow at .github/workflows/release.yml: on v*.*.* tag push, extracts the matching CHANGELOG section, creates the GitHub Release with it as the body, then publishes to PyPI via OIDC trusted publisher.

Quick wins (shipped in 1.1.0):

  • tornion keygen [--out DIR] — generates a fresh hs_ed25519_secret_key without spinning up tor.
  • tornion onion <key_dir> — prints the .onion address from an existing key dir, fully offline (reads hostname or derives from hs_ed25519_public_key via SHA3-256 + base32).
  • TORNION_KEY_DIR env var, symmetric to TORNION_TOR_PATH. Resolution order in _resolve_key_dir: explicit arg > $TORNION_KEY_DIR > <data>/hs/<app_name>/.

Shipped in 1.2.0:

  • Tor v3 client authorization (restrict who can reach your HS). Pure-Python x25519 (RFC 7748, vectors checked), server-side authorize CLI, client-side client-auth CLI, full e2e integration test. See the Client authorization section above.

Deferred to 1.x:

  • Async client (httpx.AsyncClient-style) alongside the sync one.

📄 License

MIT — see LICENSE.

tornion is an independent project, not affiliated with the Tor Project. The bundled tor binary comes from torproject.org under 3-clause BSD.

About

Consume any .onion API or publish your own in 3 lines of Python. Drop-in requests session, zero-config ASGI/WSGI server, auto-managed tor binary.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages