diff --git a/Cargo.toml b/Cargo.toml index 421f9a3..6786cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,12 @@ arti-ureq = "0.35" tor-rtcompat = "0.35" tor-circmgr = "0.35" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util"] } -futures = "0.3.14" \ No newline at end of file +futures = "0.3.14" +tor-hsservice = "0.35" +tor-proto = "0.35" +axum = { version = "0.8.1" } +tor-cell = "0.35" +safelog = { version = "0.6.0" } +hyper-util = { version = "0.1.1", features = ["tokio"] } +hyper = { version = "1", features = ["http1", "server"] } +tower = { version = "0.5.1", features = ["util", "tracing"] } \ No newline at end of file diff --git a/src/hidden_service.rs b/src/hidden_service.rs new file mode 100644 index 0000000..76209e6 --- /dev/null +++ b/src/hidden_service.rs @@ -0,0 +1,84 @@ +use axum::Router; +use axum::routing::get; +use arti_client::TorClient; +use arti_ureq::ureq::http::Request; +use futures::StreamExt; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server; +use tor_hsservice::config::OnionServiceConfigBuilder; +use tor_proto::client::stream::IncomingStreamRequest; +use safelog::{DisplayRedacted as _, sensitive}; +use tor_cell::relaycell::msg::Connected; +use tor_hsservice::StreamRequest; +use tower::Service; + +/// returns the axum router for the hidden service +/// The router will serve the given string as a response to any GET request on the root path +pub fn get_service_router(msg: String) -> Router { + let router = Router::new().route("/", get(|| async { msg })); + router +} + +/// launch the hidden service following the arti instructions. +/// The function uses both hyper and axum to serve. +/// this function launches the hidden service using the given client and router +/// then waits for requests. +pub async fn launch_hidden_service(tor_client: TorClient, router: Router){ + let svc_cfg = OnionServiceConfigBuilder::default() + .nickname("hello-world-8273649267405".parse().unwrap()) + .build() + .unwrap(); + + let (service, request_stream) = tor_client.launch_onion_service(svc_cfg).unwrap(); + println!("service is running on: {}", service.onion_address().unwrap().display_unredacted()); + + eprintln!("waiting for service to become fully reachable"); + while let Some(status) = service.status_events().next().await { + if status.state().is_fully_reachable() { + break; + } + } + + let stream_requests = tor_hsservice::handle_rend_requests(request_stream); + tokio::pin!(stream_requests); + eprintln!("ready to serve connections"); + + while let Some(stream_request) = stream_requests.next().await { + let router = router.clone(); + + tokio::spawn(async move { + let request = stream_request.request().clone(); + if let Err(err) = handle_requests(stream_request, router).await { + eprintln!("error serving connection {:?}: {}", sensitive(request), err); + }; + }); + } + + drop(service); + eprintln!("onion service exited cleanly"); +} + +/// this function serves the incoming requests to the hidden service using the given stream and router +async fn handle_requests(stream_request: StreamRequest, router: Router) -> anyhow::Result<()> { + match stream_request.request() { + IncomingStreamRequest::Begin(begin) if begin.port() == 80 => { + let onion_service_stream = stream_request.accept(Connected::new_empty()).await?; + let io = TokioIo::new(onion_service_stream); + + let hyper_service = hyper::service::service_fn(move |request: Request| { + router.clone().call(request) + }); + + server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection(io, hyper_service) + .await + .map_err(|x| anyhow::anyhow!(x))?; + } + _ => { + stream_request.shutdown_circuit()?; + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index d8d6920..25c8c07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod ureq_builder; pub mod tests; -pub mod tor_client_builder; \ No newline at end of file +pub mod tor_client_builder; +pub mod hidden_service; \ No newline at end of file diff --git a/src/tests/get_request_tests.rs b/src/tests/get_request_tests.rs index 9b42641..84bf0ff 100644 --- a/src/tests/get_request_tests.rs +++ b/src/tests/get_request_tests.rs @@ -1,5 +1,5 @@ #[cfg(test)] -mod tests { +pub mod tests { use anyhow::Result; use arti_client::TorClient; use crate::ureq_builder::*; @@ -12,7 +12,7 @@ mod tests { const TEST_URL_PATH_HTTP_PLAIN: &str = "/api/ip"; // We will use the following onion address with the tor client. const TEST_ONION: &str = "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion"; - const TEST_ONION_PATH: &str = "/"; + pub const TEST_ONION_PATH: &str = "/"; /// Builds all components fn build_ureq_agent() -> Result { let tls = choose_tls_provider(); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d9a41cf..a4c3f1b 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1 +1,2 @@ -pub mod get_request_tests; \ No newline at end of file +pub mod get_request_tests; +pub mod test_hidden_service; \ No newline at end of file diff --git a/src/tests/test_hidden_service.rs b/src/tests/test_hidden_service.rs new file mode 100644 index 0000000..98c24a4 --- /dev/null +++ b/src/tests/test_hidden_service.rs @@ -0,0 +1,44 @@ + +#[cfg(test)] +mod tests { + use crate::tests::get_request_tests::tests::TEST_ONION_PATH; + use anyhow::Result; + use arti_client::TorClient; + use crate::hidden_service::{get_service_router, launch_hidden_service}; + use crate::tor_client_builder::{ fetch_with_client, make_tor_client_config}; + + // we will use this as the output of the hidden service + const HELLO_WORLD: &str = "Hello, world!"; + + /// Test: launch a `hello_world` hidden service + /// This test may take a little while to run + /// Warning: this will not terminate. + #[tokio::test(flavor = "multi_thread")] + async fn test_launch_hidden_service() -> Result<()> { + let config = make_tor_client_config(); + + let tor_client = TorClient::create_bootstrapped(config).await.unwrap(); + + let router = get_service_router(HELLO_WORLD.to_string()); + launch_hidden_service(tor_client, router).await; + + Ok(()) + } + + /// Test: send get request to onion address over Tor using the tor client. + /// again this is http and no tls + /// warning: this will print out an entire html page + async fn fetches_onion_url_over_tor_client(onion_url: &str) -> Result<()> { + let config = make_tor_client_config(); + + let tor_client = TorClient::builder() + .config(config) + .create_unbootstrapped()?; + + let body = fetch_with_client(tor_client, onion_url, TEST_ONION_PATH).await?; + //sanity check: + assert!(!body.trim().is_empty(), "empty body from {}", onion_url); + Ok(()) + } + +} \ No newline at end of file diff --git a/src/ureq_builder.rs b/src/ureq_builder.rs index 90ffe69..476fff3 100644 --- a/src/ureq_builder.rs +++ b/src/ureq_builder.rs @@ -5,10 +5,10 @@ use arti_ureq::ureq::tls::RootCerts; /// Build a Connector from the Tor client and chosen TLS provider. /// This is a ureq connector, and here we are giving it out already created tor client + tls pub fn make_connector( - tor_client: arti_client::TorClient, + tor_client: arti_client::TorClient, tls_provider: ureq::tls::TlsProvider, -) -> Result> { - let builder = arti_ureq::Connector::::builder() +) -> Result> { + let builder = arti_ureq::Connector::::builder() .context("Failed to create ConnectorBuilder")? .tor_client(tor_client) .tls_provider(tls_provider); @@ -18,7 +18,7 @@ pub fn make_connector( /// Build a `ureq::Agent` pub fn make_ureq_agent( - connector: arti_ureq::Connector, + connector: arti_ureq::Connector, tls_provider: ureq::tls::TlsProvider, ) -> Result { // Build ureq TLS config