Skip to main content

iroh_http_core/http/server/
mod.rs

1//! Incoming HTTP request — pure-Rust `serve()` implementation.
2//!
3//! Each accepted QUIC bidirectional stream is driven by hyper's HTTP/1.1
4//! server connection. The user supplies a `tower::Service<Request<Body>,
5//! Response = Response<Body>, Error = Infallible>`; the per-connection
6//! `AddExtensionLayer` makes the authenticated peer id available as a
7//! [`RemoteNodeId`] request extension (closes #177).
8//!
9//! The accept loop body lives in [`accept`]; this file is the public
10//! surface and option-resolution glue, kept close to the axum reference
11//! shape. Sub-modules: [`options`] (`ServeOptions` + defaults), [`handle`]
12//! (`ServeHandle`), [`error_layer`] (tower → HTTP error converter),
13//! [`pipeline`] / [`stack`] / [`lifecycle`] (per-bistream chain and RAII
14//! guards).
15//!
16//! The FFI-shaped callback API ([`crate::ffi::dispatcher::ffi_serve_with_callback`])
17//! is one specific consumer of this entry — it constructs an
18//! `IrohHttpService` around the JS callback and hands it in like any
19//! other service.
20
21pub(crate) mod accept;
22pub(crate) mod error_layer;
23pub(crate) mod handle;
24pub(crate) mod lifecycle;
25pub(crate) mod options;
26pub(crate) mod pipeline;
27pub(crate) mod stack;
28
29use std::{sync::Arc, time::Duration};
30
31use tower::Service;
32
33use crate::{Body, ConnectionEvent, IrohEndpoint};
34
35use self::accept::{accept_loop, AcceptConfig};
36use self::options::{
37    DEFAULT_CONCURRENCY, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_MAX_CONNECTIONS_PER_PEER,
38    DEFAULT_MAX_REQUEST_BODY_BYTES, DEFAULT_REQUEST_TIMEOUT_MS,
39};
40
41// Re-exported from sub-modules so external paths
42// (`crate::http::server::ServeOptions`, `…::ServeHandle`,
43// `…::HandleLayerErrorLayer`, `…::DEFAULT_MAX_RESPONSE_BODY_BYTES`) stay
44// unchanged after Slice C.7 split.
45pub(crate) use self::error_layer::HandleLayerErrorLayer;
46pub use self::handle::ServeHandle;
47pub use self::options::ServeOptions;
48pub(crate) use self::options::DEFAULT_MAX_RESPONSE_BODY_BYTES;
49
50// ── Connection-event callback type ───────────────────────────────────────────
51
52pub(crate) type ConnectionEventFn = Arc<dyn Fn(ConnectionEvent) + Send + Sync>;
53
54/// Authenticated peer node id of the QUIC connection a request arrived
55/// on. Inserted as a request extension by the per-connection
56/// [`tower_http::add_extension::AddExtensionLayer`] in
57/// [`serve_with_events`].
58///
59/// User-facing pure-Rust services consume it with
60/// `req.extensions().get::<RemoteNodeId>()`. Closes #177.
61#[derive(Clone, Debug)]
62pub struct RemoteNodeId(pub Arc<String>);
63
64/// Pure-Rust serve entry — convenience 3-arg wrapper that omits the
65/// connection-event callback. Equivalent to `serve_with_events(ep,
66/// opts, svc, None)`. The endpoint owns the returned serve cycle immediately,
67/// so [`IrohEndpoint::stop_serve`] works without a separate handle-registration
68/// call.
69pub fn serve<S>(endpoint: IrohEndpoint, options: ServeOptions, svc: S) -> ServeHandle
70where
71    S: Service<
72            hyper::Request<Body>,
73            Response = hyper::Response<Body>,
74            Error = std::convert::Infallible,
75        > + Clone
76        + Send
77        + Sync
78        + 'static,
79    S::Future: Send + 'static,
80{
81    serve_with_events(endpoint, options, svc, None)
82}
83
84/// Pure-Rust serve entry — the canonical inbound API.
85///
86/// Accepts any `tower::Service<Request<Body>, Response = Response<Body>,
87/// Error = Infallible>` (`Clone + Send + Sync + 'static`, with `Send`
88/// futures). Each accepted QUIC bidirectional stream is driven by
89/// hyper's HTTP/1.1 server connection through the per-connection tower
90/// stack composed in [`stack::build_stack`]; the user service sees
91/// requests with the authenticated peer id available as a typed
92/// [`RemoteNodeId`] request extension.
93///
94/// `on_connection_event` is called on 0→1 (first connection from a peer)
95/// and 1→0 (last connection from a peer closed) count transitions.
96/// The cycle is installed in `endpoint` before this function returns; adapter
97/// code may retain the returned handle, but need not register it for endpoint
98/// lifecycle methods to reach the server.
99///
100/// # Security
101///
102/// Calling this opens a **public endpoint** on the Iroh overlay network.
103/// Any peer that knows or discovers your node's public key can connect
104/// and send requests. Iroh QUIC authenticates the peer's *identity*
105/// cryptographically, but does not enforce *authorization*. Inspect
106/// [`RemoteNodeId`] in your service and reject untrusted peers.
107pub fn serve_with_events<S>(
108    endpoint: IrohEndpoint,
109    options: ServeOptions,
110    svc: S,
111    on_connection_event: Option<ConnectionEventFn>,
112) -> ServeHandle
113where
114    S: Service<
115            hyper::Request<Body>,
116            Response = hyper::Response<Body>,
117            Error = std::convert::Infallible,
118        > + Clone
119        + Send
120        + Sync
121        + 'static,
122    S::Future: Send + 'static,
123{
124    let cfg = AcceptConfig {
125        max: options.max_concurrency.unwrap_or(DEFAULT_CONCURRENCY),
126        request_timeout: options
127            .request_timeout_ms
128            .map(Duration::from_millis)
129            .unwrap_or(Duration::from_millis(DEFAULT_REQUEST_TIMEOUT_MS)),
130        max_conns_per_peer: options
131            .max_connections_per_peer
132            .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_PEER),
133        max_request_body_wire_bytes: options
134            .max_request_body_wire_bytes
135            .or(Some(DEFAULT_MAX_REQUEST_BODY_BYTES)),
136        max_request_body_decoded_bytes: options
137            .max_request_body_decoded_bytes
138            .or(Some(DEFAULT_MAX_REQUEST_BODY_BYTES)),
139        max_total_connections: options.max_total_connections,
140        drain_timeout: Duration::from_millis(
141            options.drain_timeout_ms.unwrap_or(DEFAULT_DRAIN_TIMEOUT_MS),
142        ),
143        // Load-shed is opt-out — default `true`.
144        load_shed_enabled: options.load_shed.unwrap_or(true),
145        max_header_size: endpoint.max_header_size(),
146        stack_compression: endpoint.compression().cloned(),
147        // Decompression is opt-out — default `true`.
148        decompression: options.decompression.unwrap_or(true),
149    };
150
151    let shutdown_notify = Arc::new(tokio::sync::Notify::new());
152    let close_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
153    let close_connections = Arc::new(tokio::sync::Notify::new());
154    let drain_dur = cfg.drain_timeout;
155    let (done_tx, done_rx) = tokio::sync::watch::channel(false);
156
157    // Register the cycle before spawning it. A stop racing after this point
158    // targets this exact token; Notify retains the shutdown permit if the task
159    // has not begun polling yet. The returned handle is a clone of the one in
160    // the endpoint slot, so adapter-side `set_serve_handle` is idempotent.
161    let handle = ServeHandle::pending(
162        endpoint.next_serve_token(),
163        shutdown_notify.clone(),
164        close_flag.clone(),
165        close_connections.clone(),
166        drain_dur,
167        done_rx,
168    );
169    endpoint.register_serve_handle(handle.clone());
170
171    let join = tokio::spawn(accept_loop(
172        endpoint,
173        cfg,
174        svc,
175        on_connection_event,
176        shutdown_notify.clone(),
177        close_flag.clone(),
178        close_connections.clone(),
179        done_tx,
180    ));
181    handle.attach_join(join);
182    handle
183}