Skip to main content

webtrans_quinn/
server.rs

1//! Server-side helpers for WebTransport over Quinn.
2
3#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
4use std::sync::Arc;
5use std::{num::NonZeroUsize, time::Duration};
6
7use futures::{StreamExt, future::BoxFuture, stream::FuturesUnordered};
8#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
9use rustls::pki_types::{CertificateDer, PrivateKeyDer};
10use url::Url;
11
12#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
13use crate::{CongestionControl, crypto};
14use crate::{Connect, ServerError, Session, Settings};
15
16#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
17/// Construct a WebTransport [Server] using sensible defaults.
18///
19/// This is optional; advanced users may use [Server::new] directly.
20pub struct ServerBuilder {
21    provider: crypto::Provider,
22    addr: std::net::SocketAddr,
23    transport: quinn::TransportConfig,
24    handshake_timeout: Option<Duration>,
25    max_pending_handshakes: NonZeroUsize,
26}
27
28#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
29impl Default for ServerBuilder {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
36impl ServerBuilder {
37    /// Create a server builder with sensible defaults.
38    pub fn new() -> Self {
39        Self {
40            provider: crypto::default_provider(),
41            addr: std::net::SocketAddr::from(([0_u16; 8], 443)),
42            transport: quinn::TransportConfig::default(),
43            handshake_timeout: None,
44            max_pending_handshakes: NonZeroUsize::MAX,
45        }
46    }
47
48    /// Listen on the specified address.
49    pub fn with_addr(self, addr: std::net::SocketAddr) -> Self {
50        Self { addr, ..self }
51    }
52
53    /// Enable the specified congestion controller.
54    pub fn with_congestion_control(mut self, algorithm: CongestionControl) -> Self {
55        match algorithm {
56            CongestionControl::LowLatency => self.transport.congestion_controller_factory(
57                Arc::new(quinn::congestion::NewRenoConfig::default()),
58            ),
59            CongestionControl::Throughput => self
60                .transport
61                .congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default())),
62            CongestionControl::Default => self
63                .transport
64                .congestion_controller_factory(Arc::new(quinn::congestion::CubicConfig::default())),
65        };
66
67        self
68    }
69
70    /// Replace the QUIC transport configuration.
71    ///
72    /// Use this to bound receive windows, concurrent streams, datagram buffers,
73    /// idle time, and other per-connection resources.
74    pub fn with_transport_config(mut self, transport: quinn::TransportConfig) -> Self {
75        self.transport = transport;
76        self
77    }
78
79    /// Limit the combined QUIC, HTTP/3 SETTINGS, and CONNECT handshake duration.
80    pub fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
81        self.handshake_timeout = Some(timeout);
82        self
83    }
84
85    /// Limit the number of handshakes retained before accepting another connection.
86    pub fn with_max_pending_handshakes(mut self, limit: NonZeroUsize) -> Self {
87        self.max_pending_handshakes = limit;
88        self
89    }
90
91    /// Supply a certificate used for TLS.
92    pub fn with_certificate(
93        self,
94        chain: Vec<CertificateDer<'static>>,
95        key: PrivateKeyDer<'static>,
96    ) -> Result<Server, ServerError> {
97        let mut config = rustls::ServerConfig::builder_with_provider(self.provider.clone())
98            .with_protocol_versions(&[&rustls::version::TLS13])?
99            .with_no_client_auth()
100            .with_single_cert(chain, key)?;
101
102        config.alpn_protocols = vec![crate::ALPN.as_bytes().to_vec()]; // Required ALPN.
103
104        let config: quinn::crypto::rustls::QuicServerConfig = config
105            .try_into()
106            .map_err(|_| ServerError::InvalidCryptoConfiguration)?;
107        let mut config = quinn::ServerConfig::with_crypto(Arc::new(config));
108        config.transport_config(Arc::new(self.transport));
109
110        let server = quinn::Endpoint::server(config, self.addr)
111            .map_err(|e| ServerError::IoError(e.into()))?;
112
113        Ok(Server::with_accept_limits(
114            server,
115            self.handshake_timeout,
116            self.max_pending_handshakes,
117        ))
118    }
119
120    /// Supply certificates for multiple hostnames using SNI.
121    pub fn with_certificates(
122        self,
123        certificates: Vec<(String, Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
124    ) -> Result<Server, ServerError> {
125        let mut resolver = rustls::server::ResolvesServerCertUsingSni::new();
126
127        for (name, chain, key) in certificates {
128            let certified = rustls::sign::CertifiedKey::from_der(chain, key, &self.provider)?;
129            resolver.add(&name, certified)?;
130        }
131
132        let mut config = rustls::ServerConfig::builder_with_provider(self.provider.clone())
133            .with_protocol_versions(&[&rustls::version::TLS13])?
134            .with_no_client_auth()
135            .with_cert_resolver(Arc::new(resolver));
136
137        config.alpn_protocols = vec![crate::ALPN.as_bytes().to_vec()];
138
139        let config: quinn::crypto::rustls::QuicServerConfig = config
140            .try_into()
141            .map_err(|_| ServerError::InvalidCryptoConfiguration)?;
142        let mut config = quinn::ServerConfig::with_crypto(Arc::new(config));
143        config.transport_config(Arc::new(self.transport));
144
145        let server = quinn::Endpoint::server(config, self.addr)
146            .map_err(|e| ServerError::IoError(e.into()))?;
147
148        Ok(Server::with_accept_limits(
149            server,
150            self.handshake_timeout,
151            self.max_pending_handshakes,
152        ))
153    }
154}
155
156/// A WebTransport server that accepts new sessions.
157pub struct Server {
158    endpoint: quinn::Endpoint,
159    accept: FuturesUnordered<BoxFuture<'static, Result<Request, ServerError>>>,
160    handshake_timeout: Option<Duration>,
161    max_pending_handshakes: NonZeroUsize,
162}
163
164impl Server {
165    /// Manually create a new server with a preconfigured endpoint.
166    ///
167    /// NOTE: The ALPN must be set to `crate::ALPN` for WebTransport to work.
168    pub fn new(endpoint: quinn::Endpoint) -> Self {
169        Self {
170            endpoint,
171            accept: Default::default(),
172            handshake_timeout: None,
173            max_pending_handshakes: NonZeroUsize::MAX,
174        }
175    }
176
177    fn with_accept_limits(
178        endpoint: quinn::Endpoint,
179        handshake_timeout: Option<Duration>,
180        max_pending_handshakes: NonZeroUsize,
181    ) -> Self {
182        Self {
183            endpoint,
184            accept: Default::default(),
185            handshake_timeout,
186            max_pending_handshakes,
187        }
188    }
189
190    /// Return the local address on which the server endpoint is listening.
191    pub fn local_addr(&self) -> Result<std::net::SocketAddr, std::io::Error> {
192        self.endpoint.local_addr()
193    }
194
195    /// Accept a new WebTransport session request from a client.
196    ///
197    /// Returns `None` after the endpoint is closed. Handshake failures are
198    /// returned to the caller instead of being silently discarded.
199    pub async fn accept(&mut self) -> Option<Result<Request, ServerError>> {
200        loop {
201            tokio::select! {
202                res = self.endpoint.accept(), if self.accept.len() < self.max_pending_handshakes.get() => {
203                    let conn = res?;
204                    let timeout = self.handshake_timeout;
205                    self.accept.push(Box::pin(async move {
206                        let handshake = async move {
207                            let conn = conn.await?;
208                            Request::accept(conn).await
209                        };
210                        match timeout {
211                            Some(timeout) => tokio::time::timeout(timeout, handshake)
212                                .await
213                                .map_err(|_| ServerError::HandshakeTimeout)?,
214                            None => handshake.await,
215                        }
216                    }));
217                }
218                Some(res) = self.accept.next() => {
219                    return Some(res);
220                }
221            }
222        }
223    }
224}
225
226/// A mostly complete WebTransport handshake awaiting server accept/reject based on URL.
227pub struct Request {
228    conn: Option<quinn::Connection>,
229    settings: Option<Settings>,
230    connect: Option<Connect>,
231    url: Url,
232}
233
234impl Request {
235    /// Accept a new WebTransport session from a client.
236    pub async fn accept(conn: quinn::Connection) -> Result<Self, ServerError> {
237        // Perform the HTTP/3 handshake by sending/receiving SETTINGS frames.
238        let settings = Settings::connect(&conn, false).await?;
239
240        // Accept the CONNECT request but defer the response.
241        let connect = Connect::accept(&conn).await?;
242
243        // Return the request while retaining settings/connect streams.
244        let url = connect.url().clone();
245        Ok(Self {
246            conn: Some(conn),
247            settings: Some(settings),
248            connect: Some(connect),
249            url,
250        })
251    }
252
253    /// Return the URL provided by the client.
254    pub fn url(&self) -> &Url {
255        &self.url
256    }
257
258    /// Accept the session and return a 200 OK response.
259    pub async fn ok(mut self) -> Result<Session, ServerError> {
260        let mut connect = self
261            .connect
262            .take()
263            .ok_or(ServerError::RequestAlreadyCompleted)?;
264        connect.respond(http::StatusCode::OK).await?;
265        let conn = self
266            .conn
267            .take()
268            .ok_or(ServerError::RequestAlreadyCompleted)?;
269        let settings = self
270            .settings
271            .take()
272            .ok_or(ServerError::RequestAlreadyCompleted)?;
273        Ok(Session::new(conn, settings, connect))
274    }
275
276    /// Reject the session and return the provided HTTP status code.
277    pub async fn close(mut self, status: http::StatusCode) -> Result<(), ServerError> {
278        let mut connect = self
279            .connect
280            .take()
281            .ok_or(ServerError::RequestAlreadyCompleted)?;
282        connect.reject(status).await?;
283        Ok(())
284    }
285}
286
287impl Drop for Request {
288    fn drop(&mut self) {
289        let Some(mut connect) = self.connect.take() else {
290            return;
291        };
292
293        // Keep the handshake resources alive until the explicit response has
294        // been sent. A dropped request is a server-side failure, not a silent
295        // cancellation, so use 500 rather than making the client wait for EOF.
296        let conn = self.conn.take();
297        let settings = self.settings.take();
298        let url = self.url.clone();
299        let Ok(runtime) = tokio::runtime::Handle::try_current() else {
300            tracing::error!(
301                %url,
302                "dropped unanswered WebTransport request outside a Tokio runtime; \
303                 unable to send the automatic rejection"
304            );
305            return;
306        };
307
308        runtime.spawn(async move {
309            if let Err(error) = connect
310                .reject(http::StatusCode::INTERNAL_SERVER_ERROR)
311                .await
312            {
313                tracing::warn!(
314                    %url,
315                    %error,
316                    "failed to send automatic rejection for dropped WebTransport request"
317                );
318            } else {
319                tracing::warn!(
320                    %url,
321                    status = http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
322                    "automatically rejected dropped unanswered WebTransport request"
323                );
324            }
325            drop((conn, settings));
326        });
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn builder_records_handshake_admission_limits() {
336        let limit = NonZeroUsize::new(17).unwrap();
337        let builder = ServerBuilder::new()
338            .with_handshake_timeout(Duration::from_secs(9))
339            .with_max_pending_handshakes(limit);
340        assert_eq!(builder.handshake_timeout, Some(Duration::from_secs(9)));
341        assert_eq!(builder.max_pending_handshakes, limit);
342    }
343}