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