1use 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 #[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 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 #[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 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
172pub 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
185pub 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
209pub 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
217pub 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
228pub 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 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.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}