Skip to main content

wtransport_lightyear_patch/
endpoint.rs

1use crate::config::ClientConfig;
2use crate::config::DnsResolver;
3use crate::config::Ipv6DualStackConfig;
4use crate::config::ServerConfig;
5use crate::connection::Connection;
6use crate::driver::streams::session::StreamSession;
7use crate::driver::streams::ProtoReadError;
8use crate::driver::streams::ProtoWriteError;
9use crate::driver::utils::varint_w2q;
10use crate::driver::Driver;
11use crate::error::ConnectingError;
12use crate::error::ConnectionError;
13use quinn::TokioRuntime;
14use socket2::Domain as SocketDomain;
15use socket2::Protocol as SocketProtocol;
16use socket2::Socket;
17use socket2::Type as SocketType;
18use std::collections::HashMap;
19use std::future::Future;
20use std::marker::PhantomData;
21use std::net::SocketAddr;
22use std::net::SocketAddrV4;
23use std::net::SocketAddrV6;
24use std::pin::Pin;
25use std::sync::Arc;
26use std::task::Context;
27use std::task::Poll;
28use tracing::debug;
29use url::Host;
30use url::Url;
31use wtransport_proto::error::ErrorCode;
32use wtransport_proto::frame::FrameKind;
33use wtransport_proto::headers::Headers;
34use wtransport_proto::session::ReservedHeader;
35use wtransport_proto::session::SessionRequest as SessionRequestProto;
36use wtransport_proto::session::SessionResponse as SessionResponseProto;
37
38/// Helper structure for Endpoint types.
39pub mod endpoint_side {
40    use super::*;
41
42    /// Type of endpoint accepting multiple WebTransport connections.
43    ///
44    /// Use [`Endpoint::server`] to create and server-endpoint.
45    pub struct Server {
46        pub(super) _marker: PhantomData<()>,
47    }
48
49    /// Type of endpoint opening a WebTransport connection.
50    ///
51    /// Use [`Endpoint::client`] to create and client-endpoint.
52    pub struct Client {
53        pub(super) dns_resolver: Box<dyn DnsResolver + Send + Sync>,
54    }
55}
56
57/// Entrypoint for creating client or server connections.
58///
59/// A single endpoint can be used to accept or connect multiple connections.
60/// Each endpoint internally binds an UDP socket.
61///
62/// # Server
63/// Use [`Endpoint::server`] for creating a server-side endpoint.
64/// Afterwards use the method [`Endpoint::accept`] for awaiting on incoming session request.
65///
66/// ```no_run
67/// # use anyhow::Result;
68/// # use wtransport::ServerConfig;
69/// # use wtransport::Certificate;
70/// use wtransport::Endpoint;
71///
72/// # async fn run() -> Result<()> {
73/// # let config = ServerConfig::builder()
74/// #       .with_bind_default(4433)
75/// #       .with_certificate(Certificate::load("cert.pem", "key.pem").await?)
76/// #       .build();
77/// let server = Endpoint::server(config)?;
78/// loop {
79///     let incoming_session = server.accept().await;
80///     // Spawn task that handles client incoming session...
81/// }
82/// # Ok(())
83/// # }
84/// ```
85///
86/// # Client
87/// Use [`Endpoint::client`] for creating a client-side endpoint and use [`Endpoint::connect`]
88/// to connect to a server specifying the URL.
89///
90/// ```no_run
91/// # use anyhow::Result;
92/// # use wtransport::Certificate;
93/// use wtransport::ClientConfig;
94/// use wtransport::Endpoint;
95///
96/// # async fn run() -> Result<()> {
97/// let connection = Endpoint::client(ClientConfig::default())?
98///     .connect("https://localhost:4433")
99///     .await?;
100/// # Ok(())
101/// # }
102/// ```
103pub struct Endpoint<Side> {
104    endpoint: quinn::Endpoint,
105    side: Side,
106}
107
108impl<Side> Endpoint<Side> {
109    fn bind_socket(
110        bind_address: SocketAddr,
111        dual_stack_config: Ipv6DualStackConfig,
112    ) -> std::io::Result<Socket> {
113        let domain = match bind_address {
114            SocketAddr::V4(_) => SocketDomain::IPV4,
115            SocketAddr::V6(_) => SocketDomain::IPV6,
116        };
117
118        let socket = Socket::new(domain, SocketType::DGRAM, Some(SocketProtocol::UDP))?;
119
120        match dual_stack_config {
121            Ipv6DualStackConfig::OsDefault => {}
122            Ipv6DualStackConfig::Deny => socket.set_only_v6(true)?,
123            Ipv6DualStackConfig::Allow => socket.set_only_v6(false)?,
124        }
125
126        socket.bind(&bind_address.into())?;
127
128        Ok(socket)
129    }
130
131    /// Waits for all connections on the endpoint to be cleanly shut down.
132    pub async fn wait_idle(&self) {
133        self.endpoint.wait_idle().await;
134    }
135
136    /// Gets the local [`SocketAddr`] the underlying socket is bound to.
137    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
138        self.endpoint.local_addr()
139    }
140}
141
142impl Endpoint<endpoint_side::Server> {
143    /// Constructs a *server* endpoint.
144    pub fn server(server_config: ServerConfig) -> std::io::Result<Self> {
145        let quic_config = server_config.quic_config;
146        let socket =
147            Self::bind_socket(server_config.bind_address, server_config.dual_stack_config)?;
148        let runtime = Arc::new(TokioRuntime);
149
150        let endpoint = quinn::Endpoint::new(
151            quinn::EndpointConfig::default(),
152            Some(quic_config),
153            socket.into(),
154            runtime,
155        )?;
156
157        Ok(Self {
158            endpoint,
159            side: endpoint_side::Server {
160                _marker: PhantomData,
161            },
162        })
163    }
164
165    /// Get the next incoming connection attempt from a client.
166    pub async fn accept(&self) -> IncomingSession {
167        let quic_connecting = self
168            .endpoint
169            .accept()
170            .await
171            .expect("Endpoint cannot be closed");
172
173        debug!("New incoming QUIC connection");
174
175        IncomingSession::new(quic_connecting)
176    }
177
178    /// Reloads the server configuration.
179    ///
180    /// Useful for e.g. refreshing TLS certificates without disrupting existing connections.
181    ///
182    /// # Arguments
183    ///
184    /// * `server_config` - The new configuration for the server.
185    /// * `rebind` - A boolean indicating whether the server should rebind its socket.
186    ///              If `true`, the server will bind to a new socket with the provided configuration.
187    ///              If `false`, the bind address configuration will be ignored.
188    pub fn reload_config(&self, server_config: ServerConfig, rebind: bool) -> std::io::Result<()> {
189        if rebind {
190            let socket =
191                Self::bind_socket(server_config.bind_address, server_config.dual_stack_config)?;
192            self.endpoint.rebind(socket.into())?;
193        }
194
195        let quic_config = server_config.quic_config;
196        self.endpoint.set_server_config(Some(quic_config));
197
198        Ok(())
199    }
200}
201
202impl Endpoint<endpoint_side::Client> {
203    /// Constructs a *client* endpoint.
204    pub fn client(client_config: ClientConfig) -> std::io::Result<Self> {
205        let quic_config = client_config.quic_config;
206        let socket =
207            Self::bind_socket(client_config.bind_address, client_config.dual_stack_config)?;
208        let runtime = Arc::new(TokioRuntime);
209
210        let mut endpoint = quinn::Endpoint::new(
211            quinn::EndpointConfig::default(),
212            None,
213            socket.into(),
214            runtime,
215        )?;
216
217        endpoint.set_default_client_config(quic_config);
218
219        Ok(Self {
220            endpoint,
221            side: endpoint_side::Client {
222                dns_resolver: client_config.dns_resolver,
223            },
224        })
225    }
226
227    /// Establishes a WebTransport connection to a specified URL.
228    ///
229    /// This method initiates a WebTransport connection to the specified URL.
230    /// It validates the URL, and performs necessary steps to establish a secure connection.
231    ///
232    /// # Arguments
233    ///
234    /// * `options` - Connection options specifying the URL and additional headers.
235    ///               It can be simply an [URL](https://en.wikipedia.org/wiki/URL) string representing
236    ///               the WebTransport endpoint to connect to. It must have an `https` scheme.
237    ///               The URL can specify either an IP address or a hostname.
238    ///               When specifying a hostname, the method will internally perform DNS resolution,
239    ///               configured with
240    ///               [`ClientConfigBuilder::dns_resolver`](crate::config::ClientConfigBuilder::dns_resolver).
241    ///
242    /// # Examples
243    ///
244    /// Connect using a URL with a hostname (DNS resolution is performed):
245    ///
246    /// ```no_run
247    /// # use anyhow::Result;
248    /// # use wtransport::endpoint::endpoint_side::Client;
249    /// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
250    /// let url = "https://example.com:4433/webtransport";
251    /// let connection = endpoint.connect(url).await?;
252    /// # Ok(())
253    /// # }
254    /// ```
255    ///
256    /// Connect using a URL with an IP address:
257    ///
258    /// ```no_run
259    /// # use anyhow::Result;
260    /// # use wtransport::endpoint::endpoint_side::Client;
261    /// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
262    /// let url = "https://127.0.0.1:4343/webtransport";
263    /// let connection = endpoint.connect(url).await?;
264    /// # Ok(())
265    /// # }
266    /// ```
267    ///
268    /// Connect adding an additional header:
269    ///
270    /// ```no_run
271    /// # use anyhow::Result;
272    /// # use wtransport::endpoint::endpoint_side::Client;
273    /// # use wtransport::endpoint::ConnectOptions;
274    /// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
275    /// let options = ConnectOptions::builder("https://example.com:4433/webtransport")
276    ///     .add_header("Authorization", "AuthToken")
277    ///     .build();
278    /// let connection = endpoint.connect(options).await?;
279    /// # Ok(())
280    /// # }
281    /// ```
282    pub async fn connect<O>(&self, options: O) -> Result<Connection, ConnectingError>
283    where
284        O: IntoConnectOptions,
285    {
286        let options = options.into_options();
287
288        let url = Url::parse(&options.url)
289            .map_err(|parse_error| ConnectingError::InvalidUrl(parse_error.to_string()))?;
290
291        if url.scheme() != "https" {
292            return Err(ConnectingError::InvalidUrl(
293                "WebTransport URL scheme must be 'https'".to_string(),
294            ));
295        }
296
297        let host = url.host().expect("https scheme must have an host");
298        let port = url.port().unwrap_or(443);
299
300        let (socket_address, server_name) = match host {
301            Host::Domain(domain) => {
302                let socket_address = self
303                    .side
304                    .dns_resolver
305                    .resolve(&format!("{domain}:{port}"))
306                    .await
307                    .map_err(ConnectingError::DnsLookup)?
308                    .ok_or(ConnectingError::DnsNotFound)?;
309
310                (socket_address, domain.to_string())
311            }
312            Host::Ipv4(address) => {
313                let socket_address = SocketAddr::V4(SocketAddrV4::new(address, port));
314                (socket_address, address.to_string())
315            }
316            Host::Ipv6(address) => {
317                let socket_address = SocketAddr::V6(SocketAddrV6::new(address, port, 0, 0));
318                (socket_address, address.to_string())
319            }
320        };
321
322        let quic_connection = self
323            .endpoint
324            .connect(socket_address, &server_name)
325            .expect("QUIC connection parameters must be validated")
326            .await
327            .map_err(|connection_error| {
328                ConnectingError::ConnectionError(connection_error.into())
329            })?;
330
331        let driver = Driver::init(quic_connection.clone());
332
333        let _settings = driver.accept_settings().await.map_err(|driver_error| {
334            ConnectingError::ConnectionError(ConnectionError::with_driver_error(
335                driver_error,
336                &quic_connection,
337            ))
338        })?;
339
340        // TODO(biagio): validate settings
341
342        let mut session_request_proto =
343            SessionRequestProto::new(url.as_ref()).expect("Url has been already validate");
344
345        for (k, v) in options.additional_headers {
346            session_request_proto
347                .insert(k.clone(), v)
348                .map_err(|ReservedHeader| ConnectingError::ReservedHeader(k))?;
349        }
350
351        let mut stream_session = match driver.open_session(session_request_proto).await {
352            Ok(stream_session) => stream_session,
353            Err(driver_error) => {
354                return Err(ConnectingError::ConnectionError(
355                    ConnectionError::with_driver_error(driver_error, &quic_connection),
356                ))
357            }
358        };
359
360        let stream_id = stream_session.id();
361        let session_id = stream_session.session_id();
362
363        match stream_session
364            .write_frame(stream_session.request().headers().generate_frame(stream_id))
365            .await
366        {
367            Ok(()) => {}
368            Err(ProtoWriteError::Stopped) => {
369                return Err(ConnectingError::SessionRejected);
370            }
371            Err(ProtoWriteError::NotConnected) => {
372                return Err(ConnectingError::with_no_connection(&quic_connection));
373            }
374        }
375
376        let frame = loop {
377            let frame = match stream_session.read_frame().await {
378                Ok(frame) => frame,
379                Err(ProtoReadError::H3(error_code)) => {
380                    quic_connection.close(varint_w2q(error_code.to_code()), b"");
381                    return Err(ConnectingError::ConnectionError(
382                        ConnectionError::local_h3_error(error_code),
383                    ));
384                }
385                Err(ProtoReadError::IO(_io_error)) => {
386                    return Err(ConnectingError::with_no_connection(&quic_connection));
387                }
388            };
389
390            if let FrameKind::Exercise(_) = frame.kind() {
391                continue;
392            }
393            break frame;
394        };
395
396        if !matches!(frame.kind(), FrameKind::Headers) {
397            quic_connection.close(varint_w2q(ErrorCode::FrameUnexpected.to_code()), b"");
398            return Err(ConnectingError::ConnectionError(
399                ConnectionError::local_h3_error(ErrorCode::FrameUnexpected),
400            ));
401        }
402
403        let headers = match Headers::with_frame(&frame, stream_id) {
404            Ok(headers) => headers,
405            Err(error_code) => {
406                quic_connection.close(varint_w2q(error_code.to_code()), b"");
407                return Err(ConnectingError::ConnectionError(
408                    ConnectionError::local_h3_error(error_code),
409                ));
410            }
411        };
412
413        let session_response = match SessionResponseProto::try_from(headers) {
414            Ok(session_response) => session_response,
415            Err(_) => {
416                quic_connection.close(varint_w2q(ErrorCode::Message.to_code()), b"");
417                return Err(ConnectingError::ConnectionError(
418                    ConnectionError::local_h3_error(ErrorCode::Message),
419                ));
420            }
421        };
422
423        if session_response.code().is_successful() {
424            match driver.register_session(stream_session).await {
425                Ok(()) => {}
426                Err(driver_error) => {
427                    return Err(ConnectingError::ConnectionError(
428                        ConnectionError::with_driver_error(driver_error, &quic_connection),
429                    ))
430                }
431            }
432        } else {
433            return Err(ConnectingError::SessionRejected);
434        }
435
436        Ok(Connection::new(quic_connection, driver, session_id))
437    }
438}
439
440/// Options for establishing a client WebTransport connection.
441///
442/// Used in [`Endpoint::connect`].
443///
444/// # Examples
445///
446/// ```no_run
447/// # use anyhow::Result;
448/// # use wtransport::endpoint::endpoint_side::Client;
449/// # use wtransport::endpoint::ConnectOptions;
450/// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
451/// let options = ConnectOptions::builder("https://example.com:4433/webtransport")
452///     .add_header("Authorization", "AuthToken")
453///     .build();
454/// let connection = endpoint.connect(options).await?;
455/// # Ok(())
456/// # }
457/// ```
458pub struct ConnectOptions {
459    url: String,
460    additional_headers: HashMap<String, String>,
461}
462
463impl ConnectOptions {
464    /// Creates a new `ConnectOptions` using a builder pattern.
465    ///
466    /// # Arguments
467    ///
468    /// * `url` - A [URL](https://en.wikipedia.org/wiki/URL) string representing the WebTransport
469    ///           endpoint to connect to. It must have an `https` scheme.
470    ///           The URL can specify either an IP address or a hostname.
471    ///           When specifying a hostname, the method will internally perform DNS resolution,
472    ///           configured with
473    ///           [`ClientConfigBuilder::dns_resolver`](crate::config::ClientConfigBuilder::dns_resolver).
474    pub fn builder<S>(url: S) -> ConnectRequestBuilder
475    where
476        S: ToString,
477    {
478        ConnectRequestBuilder {
479            url: url.to_string(),
480            additional_headers: Default::default(),
481        }
482    }
483}
484
485/// A trait for converting types into `ConnectOptions`.
486pub trait IntoConnectOptions {
487    /// Perform value-to-value conversion into [`ConnectOptions`].
488    fn into_options(self) -> ConnectOptions;
489}
490
491/// A builder for [`ConnectOptions`].
492///
493/// See [`ConnectOptions::builder`].
494pub struct ConnectRequestBuilder {
495    url: String,
496    additional_headers: HashMap<String, String>,
497}
498
499impl ConnectRequestBuilder {
500    /// Adds a header to the connection options.
501    ///
502    /// # Examples
503    ///
504    /// ```rust
505    /// use wtransport::endpoint::ConnectOptions;
506    ///
507    /// let options = ConnectOptions::builder("https://example.com:4433/webtransport")
508    ///     .add_header("Authorization", "AuthToken")
509    ///     .build();
510    /// ```
511    pub fn add_header<K, V>(mut self, key: K, value: V) -> Self
512    where
513        K: ToString,
514        V: ToString,
515    {
516        self.additional_headers
517            .insert(key.to_string(), value.to_string());
518        self
519    }
520
521    /// Constructs the [`ConnectOptions`] from the builder configuration.
522    pub fn build(self) -> ConnectOptions {
523        ConnectOptions {
524            url: self.url,
525            additional_headers: self.additional_headers,
526        }
527    }
528}
529
530impl IntoConnectOptions for ConnectRequestBuilder {
531    fn into_options(self) -> ConnectOptions {
532        self.build()
533    }
534}
535
536impl IntoConnectOptions for ConnectOptions {
537    fn into_options(self) -> ConnectOptions {
538        self
539    }
540}
541
542impl<S> IntoConnectOptions for S
543where
544    S: ToString,
545{
546    fn into_options(self) -> ConnectOptions {
547        ConnectOptions::builder(self).build()
548    }
549}
550
551type DynFutureIncomingSession =
552    dyn Future<Output = Result<SessionRequest, ConnectionError>> + Send + Sync;
553
554/// [`Future`] for an in-progress incoming connection attempt.
555///
556/// Created by [`Endpoint::accept`].
557pub struct IncomingSession(Pin<Box<DynFutureIncomingSession>>);
558
559impl IncomingSession {
560    fn new(quic_connecting: quinn::Connecting) -> Self {
561        Self(Box::pin(Self::accept(quic_connecting)))
562    }
563
564    async fn accept(quic_connecting: quinn::Connecting) -> Result<SessionRequest, ConnectionError> {
565        let quic_connection = quic_connecting.await?;
566
567        let driver = Driver::init(quic_connection.clone());
568
569        let _settings = driver.accept_settings().await.map_err(|driver_error| {
570            ConnectionError::with_driver_error(driver_error, &quic_connection)
571        })?;
572
573        // TODO(biagio): validate settings
574
575        let stream_session = driver.accept_session().await.map_err(|driver_error| {
576            ConnectionError::with_driver_error(driver_error, &quic_connection)
577        })?;
578
579        Ok(SessionRequest::new(quic_connection, driver, stream_session))
580    }
581}
582
583impl Future for IncomingSession {
584    type Output = Result<SessionRequest, ConnectionError>;
585
586    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
587        Future::poll(self.0.as_mut(), cx)
588    }
589}
590
591/// A incoming client session request.
592///
593/// Server should use methods [`accept`](Self::accept), [`forbidden`](Self::forbidden),
594/// or [`not_found`](Self::not_found) in order to validate or reject the client request.
595pub struct SessionRequest {
596    quic_connection: quinn::Connection,
597    driver: Driver,
598    stream_session: StreamSession,
599}
600
601impl SessionRequest {
602    pub(crate) fn new(
603        quic_connection: quinn::Connection,
604        driver: Driver,
605        stream_session: StreamSession,
606    ) -> Self {
607        Self {
608            quic_connection,
609            driver,
610            stream_session,
611        }
612    }
613
614    /// Returns the `:authority` field of the request.
615    pub fn authority(&self) -> &str {
616        self.stream_session.request().authority()
617    }
618
619    /// Returns the `:path` field of the request.
620    pub fn path(&self) -> &str {
621        self.stream_session.request().path()
622    }
623
624    /// Returns the `origin` field of the request if present.
625    pub fn origin(&self) -> Option<&str> {
626        self.stream_session.request().origin()
627    }
628
629    /// Returns the `user-agent` field of the request if present.
630    pub fn user_agent(&self) -> Option<&str> {
631        self.stream_session.request().user_agent()
632    }
633
634    /// Returns all header fields associated with the request.
635    pub fn headers(&self) -> &HashMap<String, String> {
636        self.stream_session.request().headers().as_ref()
637    }
638
639    /// Accepts the client request and it establishes the WebTransport session.
640    pub async fn accept(mut self) -> Result<Connection, ConnectionError> {
641        let user_agent = self.user_agent().unwrap_or_default();
642
643        let mut response = SessionResponseProto::ok();
644
645        // Chrome support
646        if !user_agent.contains("firefox") {
647            response.add("sec-webtransport-http3-draft", "draft02");
648        }
649
650        self.send_response(response).await?;
651
652        let session_id = self.stream_session.session_id();
653
654        self.driver
655            .register_session(self.stream_session)
656            .await
657            .map_err(|driver_error| {
658                ConnectionError::with_driver_error(driver_error, &self.quic_connection)
659            })?;
660
661        Ok(Connection::new(
662            self.quic_connection,
663            self.driver,
664            session_id,
665        ))
666    }
667
668    /// Rejects the client request by replying with `403` status code.
669    pub async fn forbidden(self) {
670        self.reject(SessionResponseProto::forbidden()).await;
671    }
672
673    /// Rejects the client request by replying with `404` status code.
674    pub async fn not_found(self) {
675        self.reject(SessionResponseProto::not_found()).await;
676    }
677
678    async fn reject(mut self, mut response: SessionResponseProto) {
679        let user_agent = self.user_agent().unwrap_or_default();
680
681        // Chrome support
682        if !user_agent.contains("firefox") {
683            response.add("sec-webtransport-http3-draft", "draft02");
684        }
685
686        let _ = self.send_response(response).await;
687        self.stream_session.finish().await;
688    }
689
690    async fn send_response(
691        &mut self,
692        response: SessionResponseProto,
693    ) -> Result<(), ConnectionError> {
694        let frame = response.headers().generate_frame(self.stream_session.id());
695
696        match self.stream_session.write_frame(frame).await {
697            Ok(()) => Ok(()),
698            Err(ProtoWriteError::NotConnected) => {
699                Err(ConnectionError::no_connect(&self.quic_connection))
700            }
701            Err(ProtoWriteError::Stopped) => {
702                self.quic_connection
703                    .close(varint_w2q(ErrorCode::ClosedCriticalStream.to_code()), b"");
704
705                Err(ConnectionError::local_h3_error(
706                    ErrorCode::ClosedCriticalStream,
707                ))
708            }
709        }
710    }
711}