Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,327 changes: 3,163 additions & 164 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ rustls = { version = "0.23.21", default-features = false, features = ["aws_lc_rs
# Tower Services
tower = { version = "0.5.2", default-features = false, features = ["make"], optional = true }

# Arti
arti-client = { version = "0.27.0", default-features = false, features = ["compression", "rustls", "tokio", "onion-service-service"], optional = true }
tor-hsservice = { version = "0.27.0", optional = true }
tor-proto = { version = "0.27.0", optional = true }
tor-cell = { version = "0.27.0", optional = true }

scuffle-workspace-hack.workspace = true

[dev-dependencies]
Expand All @@ -66,6 +72,7 @@ http3 = ["dep:quinn", "dep:h3-quinn", "dep:h3"]
tls-rustls = ["dep:tokio-rustls", "dep:rustls"]
http3-tls-rustls = ["http3", "tls-rustls"]
tower = ["dep:tower"]
arti = ["dep:arti-client", "dep:tor-hsservice", "dep:tor-proto", "dep:tor-cell"]

[package.metadata.docs.rs]
all-features = true
Expand Down
6 changes: 5 additions & 1 deletion crates/http/examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ path = "src/axum.rs"
name = "scuffle-http-simple-service"
path = "src/simple_service.rs"

[[example]]
name = "scuffle-http-axum-onion"
path = "src/axum_onion.rs"

[dev-dependencies]
axum = { version = "0.8.1", features = ["macros", "ws"] }
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
rustls = "0.23.21"
rustls-pemfile = "2.2.0"
scuffle-http = { workspace = true, features = ["tls-rustls", "http3", "tracing"] }
scuffle-http = { workspace = true, features = ["tls-rustls", "http3", "tracing", "arti"] }
scuffle-workspace-hack.workspace = true
40 changes: 40 additions & 0 deletions crates/http/examples/src/axum_onion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use axum::http::Request;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, fmt};

async fn hello_world(req: Request<axum::body::Body>) -> axum::response::Response<String> {
tracing::info!("received request: {} {}", req.method(), req.uri());
axum::response::Response::new("Hello, World!\n".to_string())
}

#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();

let make_service = axum::Router::<()>::new()
.route("/", axum::routing::get(hello_world))
.into_make_service();

scuffle_http::HttpServer::builder()
.service_factory(scuffle_http::service::tower_make_service_factory(make_service))
.bind("[::]:80".parse().unwrap())
.onion_service_config(
scuffle_http::backend::arti::OnionServiceConfigBuilder::default()
.nickname("test".parse().unwrap())
.build()
.unwrap(),
)
.build()
.run()
.await
.unwrap();
}
146 changes: 146 additions & 0 deletions crates/http/src/backend/arti/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use std::net::{IpAddr, SocketAddr};

use arti_client::{TorClient, TorClientConfig};
use futures::StreamExt;
use tor_cell::relaycell::msg::Connected;
pub use tor_hsservice::config::{OnionServiceConfig, OnionServiceConfigBuilder};
use tor_proto::stream::IncomingStreamRequest;
#[cfg(feature = "tracing")]
use tracing::Instrument;

use crate::service::{HttpService, HttpServiceFactory};

#[derive(Debug, Clone, bon::Builder)]
pub struct ArtiBackend<F> {
/// The [`scuffle_context::Context`] this server will live by.
#[builder(default = scuffle_context::Context::global())]
ctx: scuffle_context::Context,
/// The service factory that will be used to create new services.
service_factory: F,
#[builder(default = TorClientConfig::default())]
tor_client_config: TorClientConfig,
onion_service_config: OnionServiceConfig,
bind_port: u16,
/// Enable HTTP/1.1.
#[cfg(feature = "http1")]
#[cfg_attr(docsrs, doc(cfg(feature = "http1")))]
#[builder(default = true)]
http1_enabled: bool,
/// Enable HTTP/2.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
#[builder(default = true)]
http2_enabled: bool,
}

impl<F> ArtiBackend<F>
where
F: HttpServiceFactory + Clone + Send + 'static,
F::Error: std::error::Error + Send,
F::Service: Clone + Send + 'static,
<F::Service as HttpService>::Error: std::error::Error + Send + Sync,
<F::Service as HttpService>::ResBody: Send,
<<F::Service as HttpService>::ResBody as http_body::Body>::Data: Send,
<<F::Service as HttpService>::ResBody as http_body::Body>::Error: std::error::Error + Send + Sync,
{
pub async fn run(self) -> Result<(), crate::error::Error<F>> {
let client = TorClient::create_bootstrapped(self.tor_client_config).await?;
let (service, request_stream) = client.launch_onion_service(self.onion_service_config)?;

#[cfg(feature = "tracing")]
if let Some(id) = service.onion_name() {
tracing::info!(onion_address = %id, "onion service started");
}

let stream_requests = tor_hsservice::handle_rend_requests(request_stream);
tokio::pin!(stream_requests);

#[cfg(feature = "tracing")]
tracing::debug!("listening for incoming connections");

while let Some(stream_request) = stream_requests.next().await {
let ctx = self.ctx.clone();
let mut service_factory = self.service_factory.clone();

let connection_fut = async move {
match stream_request.request() {
IncomingStreamRequest::Begin(begin) => {
if begin.port() != self.bind_port {
#[cfg(feature = "tracing")]
tracing::warn!(incoming_port = %begin.port(), bind_port = %self.bind_port, "port mismatch");
return;
}

#[cfg(feature = "tracing")]
tracing::trace!("accepting new connection");

// workaround
let null_addr = SocketAddr::new(IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), 0);

// make a new service
let http_service = match service_factory.new_service(null_addr).await {
Ok(service) => service,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!(err = %_e, "failed to create service");
return;
}
};

let onion_service_stream = match stream_request.accept(Connected::new_empty()).await {
Ok(stream) => stream,
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!(err = %_e, "failed to accept stream");
return;
}
};

#[cfg(feature = "http1")]
let http1 = self.http1_enabled;
#[cfg(not(feature = "http1"))]
let http1 = false;

#[cfg(feature = "http2")]
let http2 = self.http2_enabled;
#[cfg(not(feature = "http2"))]
let http2 = false;

let _res = crate::backend::hyper::handler::handle_connection::<F, _, _>(
ctx,
http_service,
onion_service_stream,
http1,
http2,
)
.await;

#[cfg(feature = "tracing")]
if let Err(e) = _res {
tracing::warn!(err = %e, "error handling connection");
}

#[cfg(feature = "tracing")]
tracing::trace!("connection closed");
}
_ => {
#[cfg(feature = "tracing")]
tracing::info!("closing circuit");

if let Err(_e) = stream_request.shutdown_circuit() {
#[cfg(feature = "tracing")]
tracing::warn!(err = %_e, "failed to shutdown circuit");
}
}
}
};

#[cfg(feature = "tracing")]
let connection_fut = connection_fut.instrument(tracing::info_span!("connection"));

tokio::spawn(connection_fut);
}

Ok(())
}
}
2 changes: 1 addition & 1 deletion crates/http/src/backend/hyper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use tracing::Instrument;
use crate::error::Error;
use crate::service::{HttpService, HttpServiceFactory};

mod handler;
pub(crate) mod handler;
mod stream;
mod utils;

Expand Down
3 changes: 3 additions & 0 deletions crates/http/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//!
//! You probably don't want to use this module directly and should instead use the [`HttpServer`](crate::HttpServer) struct.

#[cfg(feature = "arti")]
#[cfg_attr(docsrs, doc(cfg(feature = "arti")))]
pub mod arti;
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(feature = "http3")))]
pub mod h3;
Expand Down
4 changes: 4 additions & 0 deletions crates/http/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ where
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(feature = "http3")))]
QuinnConnection(#[from] h3_quinn::quinn::ConnectionError),
#[error("arti error: {0}")]
#[cfg(feature = "arti")]
#[cfg_attr(docsrs, doc(cfg(feature = "arti")))]
Arti(#[from] arti_client::Error),
#[error("make service error: {0}")]
ServiceFactoryError(F::Error),
#[error("service error: {0}")]
Expand Down
69 changes: 69 additions & 0 deletions crates/http/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,72 @@ pub struct HttpServer<F> {
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(feature = "http3")))]
enable_http3: bool,
#[builder(setters(vis = "", name = onion_service_config_internal))]
#[cfg(feature = "arti")]
#[cfg_attr(docsrs, doc(cfg(feature = "arti")))]
onion_service_config: Option<tor_hsservice::OnionServiceConfig>,
#[builder(setters(vis = "", name = tor_client_config_internal))]
#[cfg(feature = "arti")]
#[cfg_attr(docsrs, doc(cfg(feature = "arti")))]
tor_client_config: Option<arti_client::TorClientConfig>,
/// rustls config.
///
/// Use this field to set the server into TLS mode.
/// It will only accept TLS connections when this is set.
#[builder(setters(vis = "", name = rustls_config_internal))]
#[cfg(feature = "tls-rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "tls-rustls")))]
rustls_config: Option<rustls::ServerConfig>,
}

#[cfg(feature = "tls-rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "tls-rustls")))]
impl<F, S> HttpServerBuilder<F, S>
where
S: http_server_builder::State,
S::RustlsConfig: http_server_builder::IsUnset,
S::OnionServiceConfig: http_server_builder::IsUnset,
{
pub fn rustls_config(
self,
rustls_config: rustls::ServerConfig,
) -> HttpServerBuilder<F, http_server_builder::SetRustlsConfig<S>> {
self.rustls_config_internal(rustls_config)
}
}

#[cfg(feature = "arti")]
#[cfg_attr(docsrs, doc(cfg(feature = "arti")))]
impl<F, S> HttpServerBuilder<F, S>
where
S: http_server_builder::State,
S::OnionServiceConfig: http_server_builder::IsUnset,
S::RustlsConfig: http_server_builder::IsUnset,
{
pub fn onion_service_config(
self,
onion_service_config: tor_hsservice::OnionServiceConfig,
) -> HttpServerBuilder<F, http_server_builder::SetOnionServiceConfig<S>> {
self.onion_service_config_internal(onion_service_config)
}
}

#[cfg(feature = "arti")]
#[cfg_attr(docsrs, doc(cfg(feature = "arti")))]
impl<F, S> HttpServerBuilder<F, S>
where
S: http_server_builder::State,
S::OnionServiceConfig: http_server_builder::IsSet,
S::TorClientConfig: http_server_builder::IsUnset,
{
pub fn tor_client_config(
self,
tor_client_config: arti_client::TorClientConfig,
) -> HttpServerBuilder<F, http_server_builder::SetTorClientConfig<S>> {
self.tor_client_config_internal(tor_client_config)
}
}

#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(feature = "http3")))]
impl<F, S> HttpServerBuilder<F, S>
Expand Down Expand Up @@ -188,6 +245,18 @@ where
#[cfg(feature = "tls-rustls")]
self.set_alpn_protocols();

#[cfg(feature = "arti")]
if let Some(onion_service_config) = self.onion_service_config {
let backend = crate::backend::arti::ArtiBackend::builder()
.ctx(self.ctx)
.service_factory(self.service_factory)
.bind_port(self.bind.port())
.onion_service_config(onion_service_config)
.build();

return backend.run().await;
}

#[cfg(all(not(any(feature = "http1", feature = "http2")), feature = "tls-rustls"))]
let start_tcp_backend = false;
#[cfg(all(feature = "http1", not(feature = "http2")))]
Expand Down
Loading