Skip to main content

onyums/
lib.rs

1#![warn(clippy::pedantic, clippy::nursery, clippy::all, clippy::cargo)]
2#![allow(clippy::multiple_crate_versions, clippy::module_name_repetitions)]
3
4//! # Onyums
5//! Onyums is a simple axum wrapper for serving tor onion services.
6//!
7//! # Example
8//! ```rust
9//! use onyums::{serve, routing::get, Router};
10//!
11//! #[tokio::main]
12//! async fn main() {
13//!     let app = Router::new().route("/", get(|| async { "Hello, World!" }));
14//!
15//!     serve(app, "my_onion").await.unwrap();
16//! }
17//! ```
18
19use std::{net::SocketAddr, sync::LazyLock};
20
21use anyhow::{bail, Result};
22use arti_client::{config::TorClientConfigBuilder, TorClient};
23use axum::extract::connect_info::Connected as AxumConnected;
24use bytes::Bytes;
25use futures::{Stream, StreamExt};
26use http_body_util::Empty;
27use hyper::{body::Incoming, Request, Response, StatusCode};
28use hyper_util::rt::{TokioExecutor, TokioIo};
29use tokio::sync::Mutex;
30use tor_cell::relaycell::msg::Connected;
31use tor_hsservice::{config::OnionServiceConfigBuilder, HsNickname, RendRequest, RunningOnionService, StreamRequest};
32use tor_proto::client::stream::IncomingStreamRequest;
33use tor_rtcompat::tokio::TokioNativeTlsRuntime;
34use safelog::DisplayRedacted;
35use tower_service::Service;
36use tracing::{event, span, Level};
37extern crate rcgen;
38use std::sync::Arc;
39
40pub use axum::*;
41use rcgen::generate_simple_self_signed;
42use tokio_rustls::{
43	rustls, rustls::pki_types::{pem::PemObject, PrivateKeyDer, PrivatePkcs8KeyDer}, TlsAcceptor
44};
45
46static ONION_NAME: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new()));
47
48pub fn get_onion_name() -> String {
49	ONION_NAME.try_lock().map_or_else(|_| String::new(), |guard| (*guard.clone()).to_string())
50}
51
52/// Sets up and bootstraps a Tor client.
53///
54/// Uses onyums-specific state and cache directories (`./tor/onyums/state`,
55/// `./tor/onyums/cache`) rather than arti's shared `TorClientConfig::default()`
56/// location. This keeps the cache from growing without bound across runs while
57/// staying isolated from any sibling arti instance on the machine (e.g. an
58/// artiqwest client using `./tor/arti`), avoiding a state-directory collision.
59async fn setup_tor_client() -> Result<Arc<TorClient<TokioNativeTlsRuntime>>> {
60	event!(Level::INFO, "Creating Tor client...");
61	let config = TorClientConfigBuilder::from_directories("./tor/onyums/state", "./tor/onyums/cache")
62		.build()
63		.map_err(|e| anyhow::anyhow!("Failed to build Tor client config: {e}"))?;
64	let runtime = TokioNativeTlsRuntime::current().map_err(|_| anyhow::anyhow!("Failed to get current tokio runtime."))?;
65	let client = TorClient::with_runtime(runtime);
66	client.config(config).create_bootstrapped().await.map_err(|_| anyhow::anyhow!("Failed to create bootstrapped Tor client."))
67}
68
69/// Launches an onion service with the given nickname.
70fn launch_onion_service(client: &TorClient<TokioNativeTlsRuntime>, nickname: &str) -> Result<(Arc<RunningOnionService>, impl Stream<Item = RendRequest>)> {
71	event!(Level::INFO, "Launching onion service...");
72	let nickname = nickname.parse::<HsNickname>().map_err(|_| anyhow::anyhow!("Failed to parse nickname."))?;
73	let svc_cfg = OnionServiceConfigBuilder::default().nickname(nickname).build().map_err(|_| anyhow::anyhow!("Failed to build onion service config."))?;
74	client.launch_onion_service(svc_cfg)
75		.map_err(|_| anyhow::anyhow!("Failed to launch onion service."))?
76		.ok_or_else(|| anyhow::anyhow!("Onion service launch returned None"))
77}
78
79/// Retrieves and stores the onion service name.
80async fn get_and_store_onion_name(service: &Arc<RunningOnionService>) -> Result<String> {
81	event!(Level::INFO, "Getting the onion service name...");
82	let service_name = service.onion_address().ok_or_else(|| anyhow::anyhow!("Failed to get onion service name."))?.display_unredacted().to_string();
83	event!(Level::INFO, "Onion service name: {service_name}");
84
85	// Ensure we store the name with .onion suffix, but not double .onion
86	let clean_name = if service_name.ends_with(".onion.onion") {
87		service_name.strip_suffix(".onion").unwrap_or(&service_name).to_string()
88	} else if !service_name.ends_with(".onion") {
89		format!("{service_name}.onion")
90	} else {
91		service_name
92	};
93
94	event!(Level::INFO, "Cleaned onion service name: {clean_name}");
95	ONION_NAME.lock().await.clone_from(&clean_name);
96	Ok(clean_name)
97}
98
99/// Handles incoming stream requests by spawning tasks to process them.
100async fn handle_incoming_requests(mut stream_requests: impl Stream<Item = StreamRequest> + Send + Unpin, app: Router, tls_acceptor: TlsAcceptor) -> Result<()> {
101	event!(Level::INFO, "Waiting for Incoming request...");
102	while let Some(stream_request) = stream_requests.next().await {
103		let incoming_request_trace_span = span!(Level::INFO, "onyums - incoming_request");
104		let _requests_trace_guard = incoming_request_trace_span.enter();
105		event!(Level::INFO, "New incoming request found...");
106		let app = app.clone();
107		let tls_acceptor = tls_acceptor.clone();
108
109		tokio::spawn(async move {
110			// handle the incoming request
111			let result = handle_stream_request(stream_request, tls_acceptor, app.clone()).await;
112
113			if let Err(err) = result {
114				event!(Level::INFO, "Connection closed: Error handling stream request: {err}");
115			}
116		});
117	}
118	Ok(())
119}
120
121/// Initializes the onion service and returns the service and request stream.
122async fn initialize_onion_service(client: &TorClient<TokioNativeTlsRuntime>, nickname: &str) -> Result<(Arc<RunningOnionService>, impl Stream<Item = RendRequest>)> {
123	let (service, request_stream) = launch_onion_service(client, nickname)?;
124	let _service_name = get_and_store_onion_name(&service).await?;
125	Ok((service, request_stream))
126}
127
128/// Prepares the request handling stream.
129fn prepare_request_stream(request_stream: impl Stream<Item = RendRequest>) -> impl Stream<Item = StreamRequest> {
130	event!(Level::INFO, "Creating a stream to handle incoming requests...");
131	tor_hsservice::handle_rend_requests(request_stream)
132}
133
134/// Serve a web application over an onion service.
135///
136/// This function creates a new Tor client, launches an onion service, and serves a web application.
137///
138/// # Arguments
139/// `app` - The axum `Router` to serve.
140/// `nickname` - The nickname of the onion service.
141///
142/// # Returns
143/// An `anyhow::Result` indicating success or failure.
144///
145/// # Errors
146/// This function returns an error if any of the following occur:
147/// - The nickname fails to parse.
148/// - The onion service fails to launch.
149/// - The TLS acceptor fails to create.
150/// - The web server fails to start.
151/// - The Tor client fails to create.
152/// - The Tor client fails to bootstrap.
153/// - The Tor client fails to create a stream.
154/// - The Tor client fails to connect to the onion service.
155pub async fn serve(app: Router, nickname: &str) -> Result<()> {
156	let serve_trace_span = span!(Level::INFO, "onyums - serve");
157	let _info_trace_guard = serve_trace_span.enter();
158	event!(Level::INFO, "Setting up onion service...");
159
160	let client = setup_tor_client().await?;
161
162	let (service, request_stream) = initialize_onion_service(&client, nickname).await?;
163
164	let tls_acceptor = tls_acceptor()?;
165
166	let stream_requests = prepare_request_stream(request_stream);
167	tokio::pin!(stream_requests);
168
169	handle_incoming_requests(stream_requests, app, tls_acceptor).await?;
170
171	drop(service);
172	event!(Level::INFO, "Onion service exited cleanly.");
173	bail!("Onion service exited cleanly");
174}
175
176/// Handles a TLS connection on port 443.
177async fn handle_tls_connection(stream_request: StreamRequest, tls_acceptor: TlsAcceptor, app: Router) -> Result<()> {
178	event!(Level::INFO, "Accepting the incoming stream and wrapping it in a TLS stream...");
179	let onion_service_stream = stream_request.accept(Connected::new_empty()).await.map_err(|_| anyhow::anyhow!("failed to accept onion service stream"))?;
180
181	let connect_info = ConnectionInfo { circuit_id: None, socket_addr: None };
182
183	// Accept the TLS connection, logging the specific error on failure
184	let tls_onion_service_stream = tls_acceptor.accept(onion_service_stream).await.map_err(|e| anyhow::anyhow!("failed to accept TLS stream: {:?}", e))?;
185
186	// Wrap the stream in a `TokioIo` to make it compatible with tokio's `AsyncRead` and `AsyncWrite`.
187	event!(Level::INFO, "Wrapping the stream for tokio compatibility...");
188	let stream = TokioIo::new(tls_onion_service_stream);
189
190	// Hyper also has its own `Service` trait and doesn't use tower. We can use `hyper::service::service_fn` to create a hyper `Service` that calls our app through `tower::Service::call`.
191	let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
192		// We have to clone `tower_service` because hyper's `Service` uses `&self` whereas tower's `Service` requires `&mut self`.
193		// We don't need to call `poll_ready` since `Router` is always ready.
194		let connect_info = connect_info.clone();
195		let app = app.clone();
196		std::thread::spawn(move || {
197			event!(Level::INFO, "Creating tokio runtime...");
198			let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
199
200			#[allow(clippy::async_yields_async)]
201			runtime.block_on(async {
202				event!(Level::INFO, "Serving connection...");
203				app.clone().into_make_service_with_connect_info::<ConnectionInfo>().call(connect_info.clone()).await.unwrap().call(request)
204			})
205		})
206		.join()
207		.unwrap()
208	});
209
210	// Serve the connection with hyper's `auto::Builder`.
211	event!(Level::INFO, "Serving the connection with hyper...");
212	hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()).serve_connection_with_upgrades(stream, hyper_service).await.map_err(|err| anyhow::anyhow!("Error serving connection: {err}"))
213}
214
215/// Handles a plain HTTP request on port 80 by redirecting to HTTPS.
216async fn handle_http_redirect(stream_request: StreamRequest, requested_host: String) -> Result<()> {
217	event!(Level::INFO, "Accepting plain HTTP request on port 80 and redirecting to HTTPS.");
218	let onion_service_stream = stream_request.accept(Connected::new_empty()).await.map_err(|_| anyhow::anyhow!("failed to accept onion service stream"))?;
219
220	let stream = TokioIo::new(onion_service_stream);
221
222	let hyper_service = hyper::service::service_fn(move |req: Request<Incoming>| {
223		let host = requested_host.clone();
224		let path = req.uri().path_and_query().map_or("", |p| p.as_str());
225		let redirect_uri = format!("https://{host}{path}");
226		async move { Ok::<_, std::convert::Infallible>(Response::builder().status(StatusCode::MOVED_PERMANENTLY).header("Location", redirect_uri).body(Empty::<Bytes>::new()).unwrap()) }
227	});
228
229	hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()).http1_only().serve_connection(stream, hyper_service).await.map_err(|err| anyhow::anyhow!("Error serving HTTP redirect: {err}"))
230}
231
232#[derive(Clone, Debug, Default)]
233pub struct ConnectionInfo {
234	pub circuit_id: Option<String>,
235	pub socket_addr: Option<SocketAddr>,
236}
237
238impl AxumConnected<Request<Incoming>> for ConnectionInfo {
239	fn connect_info(target: Request<Incoming>) -> Self {
240		Self { circuit_id: target.extensions().get::<Self>().unwrap().circuit_id.clone(), socket_addr: None }
241	}
242}
243
244impl AxumConnected<Self> for ConnectionInfo {
245	fn connect_info(target: Self) -> Self {
246		target
247	}
248}
249
250// Move tls_acceptor function up, before serve
251fn tls_acceptor() -> Result<TlsAcceptor> {
252	let onion_name = get_onion_name();
253	let subject_alt_names = vec![onion_name];
254	let cert = generate_simple_self_signed(subject_alt_names).unwrap();
255
256	let key_der = match PrivatePkcs8KeyDer::from_pem_slice(cert.signing_key.serialize_pem().as_bytes()) {
257		Ok(key_der) => PrivateKeyDer::Pkcs8(key_der),
258		Err(e) => {
259			event!(Level::ERROR, "Error converting key to der: {:?}", e);
260			bail!(format!("Error converting key to der: {:?}", e))
261		}
262	};
263	let server_config = match rustls::ServerConfig::builder().with_no_client_auth().with_single_cert(vec![cert.cert.der().clone()], key_der) {
264		Ok(server_config) => server_config,
265		Err(e) => {
266			event!(Level::ERROR, "Error creating server config: {:?}", e);
267			bail!(format!("Error creating server config: {:?}", e))
268		}
269	};
270	let acceptor = TlsAcceptor::from(Arc::new(server_config));
271	Ok(acceptor)
272}
273
274// Then handle_stream_request
275async fn handle_stream_request(stream_request: StreamRequest, tls_acceptor: TlsAcceptor, app: Router) -> Result<()> {
276	let handling_request_trace_span = span!(Level::INFO, "onyums - handling_request");
277	let _handling_request_trace_guard = handling_request_trace_span.enter();
278	match stream_request.request().clone() {
279		// Clone request to use `begin` later
280		IncomingStreamRequest::Begin(begin) if begin.port() == 443 => {
281			// Only handle port 443 for TLS
282			handle_tls_connection(stream_request, tls_acceptor, app).await
283		}
284		IncomingStreamRequest::Begin(_begin) if _begin.port() == 80 => {
285			// Handle Port 80 (Plain HTTP) - Redirect to HTTPS
286			let onion_name = get_onion_name();
287			handle_http_redirect(stream_request, onion_name).await
288		}
289		_ => {
290			// Reject the incoming request
291			event!(Level::INFO, "Rejecting the incoming request {:?}...", stream_request.request());
292			stream_request.shutdown_circuit().map_err(|e| anyhow::anyhow!("Failed to shutdown circuit: {e}"))
293		}
294	}
295}
296
297#[cfg(test)]
298mod tests {
299	use axum::{routing::get, Router};
300
301	use super::*;
302
303	#[tokio::test]
304	async fn test_serve() {
305		let tracing_subscriber = tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).finish();
306		tracing::subscriber::set_global_default(tracing_subscriber).expect("setting default subscriber failed");
307
308		let app = Router::new().route("/", get(|| async { "Hello, World!" }));
309		let nickname = "onyums-yum-yum-test2";
310
311		match serve(app, nickname).await {
312			Ok(()) => (),
313			Err(e) => event!(Level::DEBUG, "Error serving onion service: {e}"),
314		}
315	}
316}