Skip to main content

iota_http/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::HashMap, sync::Arc, time::Duration};
6
7use connection_handler::{OnConnectionClose, sleep_or_pending};
8pub use http;
9use http::{Request, Response};
10use hyper_util::service::TowerToHyperService;
11use io::ServerIo;
12use tokio::task::JoinSet;
13use tokio_rustls::{TlsAcceptor, rustls};
14use tower::{Service, ServiceBuilder, ServiceExt};
15use tracing::trace;
16
17use self::{body::BoxBody, connection_info::ActiveConnections};
18
19pub mod body;
20mod config;
21mod connection_handler;
22mod connection_info;
23mod fuse;
24mod io;
25mod listener;
26
27pub use config::Config;
28pub use connection_info::{ConnectInfo, ConnectionId, ConnectionInfo, PeerCertificates};
29pub use listener::{Listener, ListenerExt};
30
31pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
32/// h2 alpn in plain format for rustls.
33const ALPN_H2: &[u8] = b"h2";
34/// h1 alpn in plain format for rustls.
35const ALPN_H1: &[u8] = b"http/1.1";
36
37#[derive(Default)]
38pub struct Builder {
39    config: Config,
40    tls_config: Option<rustls::ServerConfig>,
41}
42
43impl Builder {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    pub fn config(mut self, config: Config) -> Self {
49        self.config = config;
50        self
51    }
52
53    // Convenience method for configuring TLS with a single server cert
54    //
55    // Attempts to load PEM formatted files for the certificate chain and private
56    // key material from the provided file system paths.
57    pub fn tls_single_cert(
58        self,
59        cert_file: impl AsRef<std::path::Path>,
60        private_key_file: impl AsRef<std::path::Path>,
61    ) -> Result<Self, BoxError> {
62        let tls_config =
63            iota_tls::create_rustls_server_config_from_pem(cert_file, private_key_file)?;
64        Ok(self.tls_config(tls_config))
65    }
66
67    pub fn tls_config(mut self, tls_config: rustls::ServerConfig) -> Self {
68        self.tls_config = Some(tls_config);
69        self
70    }
71
72    pub fn serve<A, S, ResponseBody>(
73        self,
74        addr: A,
75        service: S,
76    ) -> Result<ServerHandle<std::net::SocketAddr>, BoxError>
77    where
78        A: std::net::ToSocketAddrs,
79        S: Service<
80                Request<BoxBody>,
81                Response = Response<ResponseBody>,
82                Error: Into<BoxError>,
83                Future: Send,
84            > + Clone
85            + Send
86            + 'static,
87        ResponseBody: http_body::Body<Data = bytes::Bytes, Error: Into<BoxError>> + Send + 'static,
88    {
89        let listener = listener::TcpListenerWithOptions::new(
90            addr,
91            self.config.tcp_nodelay,
92            self.config.tcp_keepalive,
93        )?;
94
95        Self::serve_with_listener(self, listener, service)
96    }
97
98    fn serve_with_listener<L, S, ResponseBody>(
99        self,
100        listener: L,
101        service: S,
102    ) -> Result<ServerHandle<L::Addr>, BoxError>
103    where
104        L: Listener,
105        S: Service<
106                Request<BoxBody>,
107                Response = Response<ResponseBody>,
108                Error: Into<BoxError>,
109                Future: Send,
110            > + Clone
111            + Send
112            + 'static,
113        ResponseBody: http_body::Body<Data = bytes::Bytes, Error: Into<BoxError>> + Send + 'static,
114    {
115        self.config.validate()?;
116
117        let local_addr = listener.local_addr()?;
118        let graceful_shutdown_token = tokio_util::sync::CancellationToken::new();
119        let connections = ActiveConnections::default();
120
121        let tls_config = self.tls_config.map(|mut tls| {
122            tls.alpn_protocols.push(ALPN_H2.into());
123            if self.config.accept_http1 {
124                tls.alpn_protocols.push(ALPN_H1.into());
125            }
126            Arc::new(tls)
127        });
128
129        let (watch_sender, watch_receiver) = tokio::sync::watch::channel(());
130        let server = Server {
131            config: self.config,
132            tls_config,
133            listener,
134            local_addr: local_addr.clone(),
135            service: ServiceBuilder::new()
136                .layer(tower::util::BoxCloneService::layer())
137                .map_response(|response: Response<ResponseBody>| response.map(body::boxed))
138                .map_err(Into::into)
139                .service(service),
140            pending_connections: JoinSet::new(),
141            connection_handlers: JoinSet::new(),
142            connections: connections.clone(),
143            graceful_shutdown_token: graceful_shutdown_token.clone(),
144            _watch_receiver: watch_receiver,
145        };
146
147        let handle = ServerHandle(Arc::new(HandleInner {
148            local_addr,
149            connections,
150            graceful_shutdown_token,
151            watch_sender,
152        }));
153
154        tokio::spawn(server.serve());
155
156        Ok(handle)
157    }
158}
159
160#[derive(Debug)]
161pub struct ServerHandle<A = std::net::SocketAddr>(Arc<HandleInner<A>>);
162
163#[derive(Debug)]
164struct HandleInner<A = std::net::SocketAddr> {
165    /// The local address of the server.
166    local_addr: A,
167    connections: ActiveConnections<A>,
168    graceful_shutdown_token: tokio_util::sync::CancellationToken,
169    watch_sender: tokio::sync::watch::Sender<()>,
170}
171
172impl<A> ServerHandle<A> {
173    /// Returns the local address of the server
174    pub fn local_addr(&self) -> &A {
175        &self.0.local_addr
176    }
177
178    /// Trigger a graceful shutdown of the server, but don't wait till the
179    /// server has completed shutting down
180    pub fn trigger_shutdown(&self) {
181        self.0.graceful_shutdown_token.cancel();
182    }
183
184    /// Completes once the network has been shutdown.
185    ///
186    /// This explicitly *does not* trigger the network to shutdown, see
187    /// `trigger_shutdown` or `shutdown` if you want to trigger shutting
188    /// down the server.
189    pub async fn wait_for_shutdown(&self) {
190        self.0.watch_sender.closed().await
191    }
192
193    /// Triggers a shutdown of the server and waits for it to complete shutting
194    /// down.
195    pub async fn shutdown(&self) {
196        self.trigger_shutdown();
197        self.wait_for_shutdown().await;
198    }
199
200    /// Checks if the Server has been shutdown.
201    pub fn is_shutdown(&self) -> bool {
202        self.0.watch_sender.is_closed()
203    }
204
205    pub fn connections(
206        &self,
207    ) -> std::sync::RwLockReadGuard<'_, HashMap<ConnectionId, ConnectionInfo<A>>> {
208        self.0.connections.read().unwrap()
209    }
210
211    /// Returns the number of active connections the server is handling
212    pub fn number_of_connections(&self) -> usize {
213        self.connections().len()
214    }
215}
216
217impl<A> Clone for ServerHandle<A> {
218    fn clone(&self) -> Self {
219        Self(self.0.clone())
220    }
221}
222
223type ConnectingOutput<Io, Addr> = Result<(ServerIo<Io>, Addr), crate::BoxError>;
224
225struct Server<L: Listener> {
226    config: Config,
227    tls_config: Option<Arc<rustls::ServerConfig>>,
228
229    listener: L,
230    local_addr: L::Addr,
231    service: tower::util::BoxCloneService<Request<BoxBody>, Response<BoxBody>, crate::BoxError>,
232
233    pending_connections: JoinSet<ConnectingOutput<L::Io, L::Addr>>,
234    connection_handlers: JoinSet<()>,
235    connections: ActiveConnections<L::Addr>,
236    graceful_shutdown_token: tokio_util::sync::CancellationToken,
237    // Used to signal to a ServerHandle when the server has completed shutting down
238    _watch_receiver: tokio::sync::watch::Receiver<()>,
239}
240
241impl<L> Server<L>
242where
243    L: Listener,
244{
245    async fn serve(mut self) -> Result<(), BoxError> {
246        loop {
247            tokio::select! {
248                _ = self.graceful_shutdown_token.cancelled() => {
249                    trace!("signal received, shutting down");
250                    break;
251                },
252                // While the handshake limit is reached, leave new connections in the kernel
253                // backlog rather than accepting them into an unbounded set of pending tasks.
254                (io, remote_addr) = self.listener.accept(), if self.accepts_more_connections() => {
255                    self.handle_incoming(io, remote_addr);
256                },
257                // A failed task affects only its own connection, so the loop keeps serving
258                // the others.
259                Some(maybe_connection) = self.pending_connections.join_next() => {
260                    let (io, remote_addr) = match maybe_connection {
261                        Ok(Ok((io, remote_addr))) => {
262                            (io, remote_addr)
263                        }
264                        Ok(Err(e)) => {
265                            tracing::debug!(error = %e, "error accepting connection");
266                            continue;
267                        }
268                        Err(e) => {
269                            tracing::error!(error = %e, "connection handshake task failed");
270                            continue;
271                        }
272                    };
273
274                    trace!("connection accepted");
275                    self.handle_connection(io, remote_addr);
276                },
277                Some(connection_handler_output) = self.connection_handlers.join_next() => {
278                    if let Err(e) = connection_handler_output {
279                        tracing::error!(error = %e, "connection task failed");
280                    }
281                },
282            }
283        }
284
285        // Shutting down, wait for all connection handlers to finish
286        self.shutdown().await;
287
288        Ok(())
289    }
290
291    /// Whether another connection may be accepted, or the limit on concurrent
292    /// TLS handshakes is currently reached.
293    fn accepts_more_connections(&self) -> bool {
294        self.config
295            .max_pending_connections
296            .is_none_or(|max| self.pending_connections.len() < max)
297    }
298
299    fn handle_incoming(&mut self, io: L::Io, remote_addr: L::Addr) {
300        if let Some(tls) = self.tls_config.clone() {
301            let tls_acceptor = TlsAcceptor::from(tls);
302            let allow_insecure = self.config.allow_insecure;
303            let handshake_timeout = self.config.handshake_timeout;
304            self.pending_connections.spawn(async move {
305                tokio::select! {
306                    result = handshake(io, remote_addr, tls_acceptor, allow_insecure) => result,
307                    // Dropping the handshake closes the connection, releasing its file descriptor.
308                    _ = sleep_or_pending(handshake_timeout) => Err(std::io::Error::new(
309                        std::io::ErrorKind::TimedOut,
310                        "TLS handshake timed out",
311                    )
312                    .into()),
313                }
314            });
315        } else {
316            self.handle_connection(ServerIo::new_io(io), remote_addr);
317        }
318    }
319
320    fn handle_connection(&mut self, io: ServerIo<L::Io>, remote_addr: L::Addr) {
321        let connection_shutdown_token = self.graceful_shutdown_token.child_token();
322        let connection_info = ConnectionInfo::new(
323            remote_addr,
324            io.peer_certs(),
325            connection_shutdown_token.clone(),
326        );
327        let connection_id = connection_info.id();
328        let connect_info = connection_info::ConnectInfo {
329            local_addr: self.local_addr.clone(),
330            remote_addr: connection_info.remote_address().clone(),
331        };
332        let peer_certificates = connection_info.peer_certificates().cloned();
333        let hyper_io = hyper_util::rt::TokioIo::new(io);
334
335        let hyper_svc = TowerToHyperService::new(self.service.clone().map_request(
336            move |mut request: Request<hyper::body::Incoming>| {
337                request.extensions_mut().insert(connect_info.clone());
338                if let Some(peer_certificates) = peer_certificates.clone() {
339                    request.extensions_mut().insert(peer_certificates);
340                }
341
342                request.map(body::boxed)
343            },
344        ));
345
346        self.connections
347            .write()
348            .unwrap()
349            .insert(connection_id, connection_info);
350        let on_connection_close = OnConnectionClose::new(connection_id, self.connections.clone());
351
352        self.connection_handlers
353            .spawn(connection_handler::serve_connection(
354                hyper_io,
355                hyper_svc,
356                self.config.connection_builder(),
357                connection_shutdown_token,
358                self.config.max_connection_age,
359                on_connection_close,
360            ));
361    }
362
363    async fn shutdown(mut self) {
364        // The time we are willing to wait for a connection to get gracefully shutdown
365        // before we attempt to forcefully shutdown all active connections
366        const CONNECTION_SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(1);
367
368        // Just to be careful make sure the token is canceled
369        self.graceful_shutdown_token.cancel();
370
371        // Terminate any in-progress pending connections
372        self.pending_connections.shutdown().await;
373
374        // Wait for all connection handlers to terminate
375        trace!(
376            "waiting for {} connections to close",
377            self.connection_handlers.len()
378        );
379
380        let graceful_shutdown =
381            async { while self.connection_handlers.join_next().await.is_some() {} };
382
383        if tokio::time::timeout(CONNECTION_SHUTDOWN_GRACE_PERIOD, graceful_shutdown)
384            .await
385            .is_err()
386        {
387            tracing::warn!(
388                "Failed to stop all connection handlers in {:?}. Forcing shutdown.",
389                CONNECTION_SHUTDOWN_GRACE_PERIOD
390            );
391            self.connection_handlers.shutdown().await;
392        }
393    }
394}
395
396/// Runs the pre-authentication part of accepting a connection: sniffing
397/// insecure traffic when it is allowed, then the TLS handshake.
398async fn handshake<Io, Addr>(
399    io: Io,
400    remote_addr: Addr,
401    tls_acceptor: TlsAcceptor,
402    allow_insecure: bool,
403) -> ConnectingOutput<Io, Addr>
404where
405    Io: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
406{
407    if allow_insecure {
408        // XXX: If we want to allow for supporting insecure traffic from other types of
409        // io, we'll need to implement a generic peekable IO type
410        if let Some(tcp) = <dyn std::any::Any>::downcast_ref::<tokio::net::TcpStream>(&io) {
411            // Determine whether new connection is TLS.
412            let mut buf = [0; 1];
413            // `peek` blocks until at least some data is available, so if there is no error
414            // then it must return the one byte we are requesting.
415            tcp.peek(&mut buf).await?;
416            // First byte of a TLS handshake is 0x16, so if it isn't 0x16 then its
417            // insecure
418            if buf != [0x16] {
419                tracing::trace!("accepting insecure connection");
420                return Ok((ServerIo::new_io(io), remote_addr));
421            }
422        } else {
423            tracing::warn!(
424                "'allow_insecure' is configured but io type is not 'tokio::net::TcpStream'"
425            );
426        }
427    }
428
429    tracing::trace!("accepting TLS connection");
430    let io = tls_acceptor.accept(io).await?;
431    Ok((ServerIo::new_tls_io(io), remote_addr))
432}
433
434#[cfg(test)]
435mod tests {
436    use axum::Router;
437
438    use super::*;
439
440    #[tokio::test]
441    async fn simple() {
442        const MESSAGE: &str = "Hello, World!";
443
444        let app = Router::new().route("/", axum::routing::get(|| async { MESSAGE }));
445
446        let handle = Builder::new().serve(("localhost", 0), app).unwrap();
447
448        let url = format!("http://{}", handle.local_addr());
449
450        let response = reqwest::get(url).await.unwrap().bytes().await.unwrap();
451
452        assert_eq!(response, MESSAGE.as_bytes());
453    }
454
455    #[tokio::test]
456    async fn shutdown() {
457        const MESSAGE: &str = "Hello, World!";
458
459        let app = Router::new().route("/", axum::routing::get(|| async { MESSAGE }));
460
461        let handle = Builder::new().serve(("localhost", 0), app).unwrap();
462
463        let url = format!("http://{}", handle.local_addr());
464
465        let response = reqwest::get(url).await.unwrap().bytes().await.unwrap();
466
467        // a request was just made so we should have 1 active connection
468        assert_eq!(handle.connections().len(), 1);
469
470        assert_eq!(response, MESSAGE.as_bytes());
471
472        assert!(!handle.is_shutdown());
473
474        handle.shutdown().await;
475
476        assert!(handle.is_shutdown());
477
478        // Now that the network has been shutdown there should be zero connections
479        assert_eq!(handle.connections().len(), 0);
480    }
481
482    const SERVER_NAME: &str = "iota-http-test";
483
484    /// A server config and a client config that trusts it, without client
485    /// authentication.
486    fn test_tls_configs() -> (rustls::ServerConfig, rustls::ClientConfig) {
487        use fastcrypto::{
488            ed25519::{Ed25519KeyPair, Ed25519PrivateKey},
489            traits::{KeyPair, ToFromBytes},
490        };
491
492        let keypair = Ed25519KeyPair::from(Ed25519PrivateKey::from_bytes(&[42; 32]).unwrap());
493        let public_key = keypair.public().to_owned();
494        (
495            iota_tls::create_rustls_server_config(keypair.private(), SERVER_NAME.to_string()),
496            iota_tls::create_rustls_client_config(public_key, SERVER_NAME.to_string(), None),
497        )
498    }
499
500    /// The peer is unauthenticated until its handshake completes, so a peer
501    /// that never starts one must not hold the connection open indefinitely.
502    #[tokio::test]
503    async fn silent_peer_is_dropped_after_the_handshake_timeout() {
504        use tokio::io::AsyncReadExt as _;
505
506        const HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(200);
507
508        let (server_tls_config, _) = test_tls_configs();
509        let handle = Builder::new()
510            .config(Config::default().handshake_timeout(Some(HANDSHAKE_TIMEOUT)))
511            .tls_config(server_tls_config)
512            .serve(("localhost", 0), Router::new())
513            .unwrap();
514
515        // Connect, then never send a ClientHello.
516        let mut connection = tokio::net::TcpStream::connect(handle.local_addr())
517            .await
518            .unwrap();
519
520        let mut buf = [0u8; 1];
521        let read = tokio::time::timeout(HANDSHAKE_TIMEOUT * 25, connection.read(&mut buf))
522            .await
523            .expect("the server must not wait for the handshake past the deadline");
524
525        assert!(
526            matches!(read, Ok(0) | Err(_)),
527            "the server must close the connection, got {read:?}"
528        );
529    }
530
531    /// While the pending limit is reached the server stops accepting, and
532    /// resumes once a slot frees.
533    #[tokio::test]
534    async fn pending_connections_are_capped() {
535        let (server_tls_config, client_tls_config) = test_tls_configs();
536        let handle = Builder::new()
537            .config(
538                Config::default()
539                    // Long enough that the stalled peer below is released by the test
540                    // rather than by the deadline.
541                    .handshake_timeout(Some(Duration::from_secs(60)))
542                    .max_pending_connections(Some(1)),
543            )
544            .tls_config(server_tls_config)
545            .serve(("localhost", 0), Router::new())
546            .unwrap();
547        let addr = *handle.local_addr();
548
549        // Occupy the only handshake slot with a peer that never speaks.
550        let stalled = tokio::net::TcpStream::connect(addr).await.unwrap();
551        tokio::time::sleep(Duration::from_millis(200)).await;
552
553        let connector = tokio_rustls::TlsConnector::from(Arc::new(client_tls_config));
554        let server_name = rustls::pki_types::ServerName::try_from(SERVER_NAME).unwrap();
555        let handshake = async {
556            let io = tokio::net::TcpStream::connect(addr).await.unwrap();
557            connector.connect(server_name, io).await
558        };
559        tokio::pin!(handshake);
560
561        // The connection reaches the kernel backlog but is not accepted. Without the
562        // limit the handshake completes in milliseconds, so a wide window here only
563        // guards against a slow machine, it does not weaken the assertion.
564        assert!(
565            tokio::time::timeout(Duration::from_secs(2), &mut handshake)
566                .await
567                .is_err(),
568            "the server must not handshake while the pending limit is reached"
569        );
570
571        drop(stalled);
572
573        tokio::time::timeout(Duration::from_secs(10), &mut handshake)
574            .await
575            .expect("the server must resume accepting once a slot frees")
576            .expect("the handshake must succeed");
577    }
578
579    /// A limit the accept loop can never fall below, and a limit whose slots
580    /// nothing releases, both leave the server unable to accept.
581    #[tokio::test]
582    async fn unrecoverable_limits_are_rejected() {
583        let served = |config| {
584            Builder::new()
585                .config(config)
586                .tls_config(test_tls_configs().0)
587                .serve(("localhost", 0), Router::new())
588        };
589
590        assert!(
591            served(Config::default().max_pending_connections(Some(0))).is_err(),
592            "a zero limit must be rejected"
593        );
594        assert!(
595            served(
596                Config::default()
597                    .handshake_timeout(None)
598                    .max_pending_connections(Some(8))
599            )
600            .is_err(),
601            "a limit without a handshake deadline must be rejected"
602        );
603        assert!(
604            served(
605                Config::default()
606                    .handshake_timeout(None)
607                    .max_pending_connections(None)
608            )
609            .is_ok(),
610            "removing both bounds stays allowed"
611        );
612    }
613}