Skip to main content

compio_ws/
tls.rs

1//! TLS support for WebSocket connections (native-tls and rustls).
2
3use compio_driver::AsFd;
4use compio_net::TcpStream;
5use compio_runtime::fd::PollFd;
6use compio_tls::{MaybeTlsStream, TlsConnector};
7use socket2::Socket;
8use tungstenite::{
9    Error,
10    client::{IntoClientRequest, uri_mode},
11    handshake::client::{Request, Response},
12    stream::Mode,
13};
14
15use crate::{Config, WebSocketStream, client_async_with_config};
16
17mod encryption {
18    #[cfg(feature = "native-tls")]
19    pub mod native_tls {
20        use compio_tls::{TlsConnector, native_tls};
21        use tungstenite::{Error, error::TlsError};
22
23        pub fn new_connector() -> Result<TlsConnector, Error> {
24            let native_connector = native_tls::TlsConnector::new().map_err(TlsError::from)?;
25            Ok(TlsConnector::from(native_connector))
26        }
27    }
28
29    #[cfg(feature = "rustls")]
30    pub mod rustls {
31        use std::sync::Arc;
32
33        use compio_tls::{
34            TlsConnector,
35            rustls::{ClientConfig, RootCertStore},
36        };
37        use tungstenite::Error;
38
39        fn config_with_certs() -> Result<Arc<ClientConfig>, Error> {
40            #[allow(unused_mut)]
41            let mut root_store = RootCertStore::empty();
42            #[cfg(feature = "rustls-native-certs")]
43            {
44                let rustls_native_certs::CertificateResult { certs, errors, .. } =
45                    rustls_native_certs::load_native_certs();
46
47                if !errors.is_empty() {
48                    compio_log::warn!("native root CA certificate loading errors: {errors:?}");
49                }
50
51                // Not finding any native root CA certificates is not fatal
52                // if the "webpki-roots" feature is enabled.
53                #[cfg(not(feature = "webpki-roots"))]
54                if certs.is_empty() {
55                    return Err(std::io::Error::new(
56                        std::io::ErrorKind::NotFound,
57                        format!("no native root CA certificates found (errors: {errors:?})"),
58                    )
59                    .into());
60                }
61
62                let total_number = certs.len();
63                let (number_added, number_ignored) = root_store.add_parsable_certificates(certs);
64                compio_log::debug!(
65                    "Added {number_added}/{total_number} native root certificates (ignored \
66                     {number_ignored})"
67                );
68            }
69            #[cfg(feature = "webpki-roots")]
70            {
71                root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
72            }
73
74            Ok(Arc::new(
75                ClientConfig::builder()
76                    .with_root_certificates(root_store)
77                    .with_no_client_auth(),
78            ))
79        }
80
81        #[cfg(feature = "rustls-platform-verifier")]
82        fn config_with_platform_verifier() -> Result<Arc<ClientConfig>, Error> {
83            use rustls_platform_verifier::BuilderVerifierExt;
84
85            // Use platform's native certificate verification
86            // This provides better security and enterprise integration
87            let config_result = ClientConfig::builder()
88                .with_platform_verifier()
89                .map_err(tungstenite::error::TlsError::from)?;
90            Ok(Arc::new(config_result.with_no_client_auth()))
91        }
92
93        pub fn new_connector() -> Result<TlsConnector, Error> {
94            // Create TLS connector with platform verifier when feature is
95            // enabled
96            #[cfg(feature = "rustls-platform-verifier")]
97            {
98                let config = match config_with_platform_verifier() {
99                    Ok(config_builder) => config_builder,
100                    Err(e) => {
101                        compio_log::warn!("Error creating platform verifier: {e:?}");
102                        config_with_certs()?
103                    }
104                };
105                Ok(TlsConnector::from(config))
106            }
107            #[cfg(not(feature = "rustls-platform-verifier"))]
108            {
109                // Create TLS connector with certs from enabled features
110                let config = config_with_certs()?;
111                Ok(TlsConnector::from(config))
112            }
113        }
114    }
115}
116
117async fn wrap_stream<S>(
118    socket: PollFd<S>,
119    domain: &str,
120    connector: Option<TlsConnector>,
121    mode: Mode,
122) -> Result<MaybeTlsStream<PollFd<S>>, Error>
123where
124    S: AsFd + 'static,
125{
126    match mode {
127        Mode::Plain => Ok(MaybeTlsStream::new_plain(socket)),
128        Mode::Tls => {
129            let stream = {
130                let connector = if let Some(connector) = connector {
131                    connector
132                } else {
133                    #[cfg(feature = "native-tls")]
134                    {
135                        match encryption::native_tls::new_connector() {
136                            Ok(c) => c,
137                            Err(e) => {
138                                compio_log::warn!(
139                                    "Falling back to rustls TLS connector due to native-tls \
140                                     error: {e:?}",
141                                );
142                                #[cfg(feature = "rustls")]
143                                {
144                                    encryption::rustls::new_connector()?
145                                }
146                                #[cfg(not(feature = "rustls"))]
147                                {
148                                    return Err(e);
149                                }
150                            }
151                        }
152                    }
153                    #[cfg(all(feature = "rustls", not(feature = "native-tls")))]
154                    {
155                        encryption::rustls::new_connector()?
156                    }
157                    #[cfg(not(any(feature = "native-tls", feature = "rustls")))]
158                    {
159                        return Err(Error::Url(
160                            tungstenite::error::UrlError::TlsFeatureNotEnabled,
161                        ));
162                    }
163                };
164
165                connector.connect(domain, socket).await.map_err(Error::Io)?
166            };
167            Ok(MaybeTlsStream::new_tls(stream))
168        }
169    }
170}
171
172/// Creates a WebSocket handshake from a request and a stream,
173/// upgrading the stream to TLS if required.
174pub async fn client_async_tls<R, S>(
175    request: R,
176    stream: PollFd<S>,
177) -> Result<(WebSocketStream<S>, Response), Error>
178where
179    R: IntoClientRequest,
180    S: AsFd + 'static,
181{
182    client_async_tls_with_config(request, stream, None, None).await
183}
184
185/// Similar to [`client_async_tls()`] but the one can specify a websocket
186/// configuration, and an optional connector.
187pub async fn client_async_tls_with_config<R, S>(
188    request: R,
189    stream: PollFd<S>,
190    connector: Option<TlsConnector>,
191    config: impl Into<Config>,
192) -> Result<(WebSocketStream<S>, Response), Error>
193where
194    R: IntoClientRequest,
195    S: AsFd + 'static,
196{
197    let request: Request = request.into_client_request()?;
198
199    let domain = domain(&request)?;
200
201    let mode = uri_mode(request.uri())?;
202
203    let config = config.into();
204
205    let stream = wrap_stream(stream, domain, connector, mode).await?;
206    client_async_with_config(request, stream, config).await
207}
208
209/// Connect to a given URL.
210pub async fn connect_async<R>(request: R) -> Result<(WebSocketStream<Socket>, Response), Error>
211where
212    R: IntoClientRequest,
213{
214    connect_async_with_config(request, None).await
215}
216
217/// Similar to [`connect_async()`], but user can specify a [`Config`].
218pub async fn connect_async_with_config<R>(
219    request: R,
220    config: impl Into<Config>,
221) -> Result<(WebSocketStream<Socket>, Response), Error>
222where
223    R: IntoClientRequest,
224{
225    connect_async_tls_with_config(request, config, None).await
226}
227
228/// Similar to [`connect_async()`], but user can specify a [`Config`] and an
229/// optional [`TlsConnector`].
230pub async fn connect_async_tls_with_config<R>(
231    request: R,
232    config: impl Into<Config>,
233    connector: Option<TlsConnector>,
234) -> Result<(WebSocketStream<Socket>, Response), Error>
235where
236    R: IntoClientRequest,
237{
238    let config = config.into();
239    let request: Request = request.into_client_request()?;
240
241    // We don't check if it's an IPv6 address because `std` handles it
242    // internally.
243    let domain = request
244        .uri()
245        .host()
246        .ok_or(Error::Url(tungstenite::error::UrlError::NoHostName))?;
247    let port = port(&request)?;
248
249    let socket = TcpStream::connect((domain, port))
250        .await
251        .map_err(Error::Io)?;
252    socket.set_nodelay(config.disable_nagle)?;
253    let socket = socket.into_poll_fd()?;
254    client_async_tls_with_config(request, socket, connector, config).await
255}
256
257#[inline]
258fn port(request: &Request) -> Result<u16, Error> {
259    request
260        .uri()
261        .port_u16()
262        .or_else(|| match uri_mode(request.uri()).ok()? {
263            Mode::Plain => Some(80),
264            Mode::Tls => Some(443),
265        })
266        .ok_or(Error::Url(
267            tungstenite::error::UrlError::UnsupportedUrlScheme,
268        ))
269}
270
271#[inline]
272fn domain(request: &Request) -> Result<&str, Error> {
273    request
274        .uri()
275        .host()
276        .map(|host| {
277            // If host is an IPv6 address, it might be surrounded by brackets.
278            // These brackets are *not* part of a valid IP, so they
279            // must be stripped out.
280            //
281            // The URI from the request is guaranteed to be valid, so we don't
282            // need a separate check for the closing bracket.
283
284            if host.starts_with('[') && host.ends_with(']') {
285                &host[1..host.len() - 1]
286            } else {
287                host
288            }
289        })
290        .ok_or(tungstenite::Error::Url(
291            tungstenite::error::UrlError::NoHostName,
292        ))
293}