1use 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 #[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 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 #[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 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
171pub 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
184pub 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
208pub 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
218pub 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
229pub 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 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.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}