1#[cfg(not(target_arch = "wasm32"))]
5use std::net::{IpAddr, SocketAddr};
6use std::{fmt, sync::Arc, time::Duration};
7
8use futures_util::{Sink, Stream};
9use polyoxide_rt::{BoxFuture, Runtime};
10#[cfg(not(target_arch = "wasm32"))]
11use tokio::net::TcpStream;
12
13use crate::{
14 tungstenite::{self, http::Request, Message},
15 WsError,
16};
17#[cfg(not(target_arch = "wasm32"))]
18use crate::{
19 tungstenite::{error::UrlError, http::Uri},
20 Kind,
21};
22
23pub trait SocketIo:
29 Stream<Item = Result<Message, tungstenite::Error>>
30 + Sink<Message, Error = tungstenite::Error>
31 + Send
32 + Unpin
33{
34}
35
36impl<T> SocketIo for T where
37 T: Stream<Item = Result<Message, tungstenite::Error>>
38 + Sink<Message, Error = tungstenite::Error>
39 + Send
40 + Unpin
41{
42}
43
44pub type BoxSocket = Box<dyn SocketIo>;
47
48pub trait SocketTransport: Send + Sync + fmt::Debug + 'static {
59 fn connect(
61 &self,
62 request: Request<()>,
63 timeout: Duration,
64 runtime: Arc<dyn Runtime>,
65 ) -> BoxFuture<Result<BoxSocket, WsError>>;
66
67 fn capabilities(&self) -> Capabilities;
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[non_exhaustive]
78pub struct Capabilities {
79 pub client_pings: bool,
81 pub sees_server_pings: bool,
83 pub handshake_headers: bool,
89}
90
91impl Capabilities {
92 pub const FULL: Capabilities = Capabilities::new(true, true, true);
94
95 pub const fn new(client_pings: bool, sees_server_pings: bool, handshake_headers: bool) -> Self {
97 Self {
98 client_pings,
99 sees_server_pings,
100 handshake_headers,
101 }
102 }
103}
104
105#[cfg(not(target_arch = "wasm32"))]
108pub fn default_socket_transport() -> Arc<dyn SocketTransport> {
109 Arc::new(TungsteniteTransport::new())
110}
111
112#[cfg(target_arch = "wasm32")]
115pub fn default_socket_transport() -> Arc<dyn SocketTransport> {
116 Arc::new(crate::WebSocketTransport::new())
117}
118
119#[cfg(not(target_arch = "wasm32"))]
126#[derive(Debug, Clone, Copy, Default)]
127pub struct TungsteniteTransport {
128 per_address: bool,
129}
130
131#[cfg(not(target_arch = "wasm32"))]
132impl TungsteniteTransport {
133 pub const fn new() -> Self {
136 Self { per_address: false }
137 }
138
139 pub const fn per_address() -> Self {
149 Self { per_address: true }
150 }
151}
152
153#[cfg(not(target_arch = "wasm32"))]
154impl SocketTransport for TungsteniteTransport {
155 fn connect(
156 &self,
157 request: Request<()>,
158 timeout: Duration,
159 runtime: Arc<dyn Runtime>,
160 ) -> BoxFuture<Result<BoxSocket, WsError>> {
161 let per_address = self.per_address;
162 Box::pin(async move {
163 crate::ensure_crypto_provider();
164 if per_address {
165 return dial_per_address(request, timeout, runtime.as_ref()).await;
166 }
167 let (socket, _response) = crate::connect::bounded(
168 runtime.as_ref(),
169 timeout,
170 tokio_tungstenite::connect_async(request),
171 )
172 .await??;
173 Ok::<BoxSocket, WsError>(Box::new(socket))
174 })
175 }
176
177 fn capabilities(&self) -> Capabilities {
178 Capabilities::FULL
179 }
180}
181
182#[cfg(not(target_arch = "wasm32"))]
188fn interleave_address_families(addrs: Vec<SocketAddr>) -> Vec<SocketAddr> {
189 let (v6, v4): (Vec<_>, Vec<_>) = addrs.into_iter().partition(SocketAddr::is_ipv6);
190 let mut out = Vec::with_capacity(v6.len() + v4.len());
191 let (mut v6, mut v4) = (v6.into_iter(), v4.into_iter());
192 loop {
193 match (v6.next(), v4.next()) {
194 (None, None) => return out,
195 (a, b) => out.extend(a.into_iter().chain(b)),
196 }
197 }
198}
199
200#[cfg(not(target_arch = "wasm32"))]
202fn io_error(err: std::io::Error) -> WsError {
203 WsError::from(tungstenite::Error::Io(err))
204}
205
206#[cfg(not(target_arch = "wasm32"))]
209fn no_addresses(uri: &Uri) -> WsError {
210 io_error(std::io::Error::new(
211 std::io::ErrorKind::NotFound,
212 format!("no addresses resolved for {uri}"),
213 ))
214}
215
216#[cfg(not(target_arch = "wasm32"))]
218#[derive(Debug, PartialEq, Eq)]
219enum Target<'a> {
220 Ip(IpAddr),
221 Name(&'a str),
222}
223
224#[cfg(not(target_arch = "wasm32"))]
227fn dial_target(uri: &Uri) -> Option<(Target<'_>, u16)> {
228 let host = uri.host()?;
229 let port = uri.port_u16().unwrap_or(match uri.scheme_str() {
230 Some("ws") => 80,
231 _ => 443,
232 });
233 let literal = host.trim_start_matches('[').trim_end_matches(']');
234 Some(match literal.parse::<IpAddr>() {
235 Ok(ip) => (Target::Ip(ip), port),
236 Err(_) => (Target::Name(host), port),
237 })
238}
239
240#[cfg(not(target_arch = "wasm32"))]
242async fn dial_per_address(
243 request: Request<()>,
244 timeout: Duration,
245 runtime: &dyn Runtime,
246) -> Result<BoxSocket, WsError> {
247 let uri = request.uri().clone();
248 let (target, port) = dial_target(&uri)
249 .ok_or_else(|| WsError::from(tungstenite::Error::Url(UrlError::NoHostName)))?;
250
251 let addrs: Vec<SocketAddr> = match target {
253 Target::Ip(ip) => vec![SocketAddr::new(ip, port)],
254 Target::Name(name) => tokio::net::lookup_host((name, port))
255 .await
256 .map_err(io_error)?
257 .collect(),
258 };
259
260 dial_addresses(request, addrs, timeout, runtime).await
261}
262
263#[cfg(not(target_arch = "wasm32"))]
268async fn dial_addresses(
269 request: Request<()>,
270 addrs: Vec<SocketAddr>,
271 timeout: Duration,
272 runtime: &dyn Runtime,
273) -> Result<BoxSocket, WsError> {
274 let mut last_err = no_addresses(request.uri());
275 for addr in interleave_address_families(addrs) {
276 let stream = match crate::connect::bounded(runtime, timeout, TcpStream::connect(addr)).await
277 {
278 Ok(Ok(stream)) => stream,
279 Ok(Err(err)) => {
280 last_err = io_error(err);
281 continue;
282 }
283 Err(err) => {
284 last_err = err;
285 continue;
286 }
287 };
288 match crate::connect::bounded(
290 runtime,
291 timeout,
292 crate::connect_over(request.clone(), stream),
293 )
294 .await
295 {
296 Ok(Ok(socket)) => return Ok(socket),
297 Ok(Err(err)) if matches!(err.kind(), Kind::Refused { .. }) => return Err(err),
299 Ok(Err(err)) => last_err = err,
300 Err(err) => last_err = err,
301 }
302 }
303 Err(last_err)
304}
305
306#[macro_export]
342macro_rules! transport_setters {
343 () => {};
344 ($(#[$meta:meta])* runtime; $($rest:tt)*) => {
345 $(#[$meta])*
346 pub fn runtime(
347 mut self,
348 runtime: ::std::sync::Arc<dyn $crate::__rt::Runtime>,
349 ) -> Self {
350 self.runtime = ::core::option::Option::Some(runtime);
351 self
352 }
353 $crate::transport_setters! { $($rest)* }
354 };
355 ($(#[$meta:meta])* socket_transport; $($rest:tt)*) => {
356 $(#[$meta])*
357 pub fn socket_transport(
358 mut self,
359 transport: ::std::sync::Arc<dyn $crate::SocketTransport>,
360 ) -> Self {
361 self.socket_transport = ::core::option::Option::Some(transport);
362 self
363 }
364 $crate::transport_setters! { $($rest)* }
365 };
366}
367
368#[cfg(test)]
369mod tests {
370 use std::{sync::Arc, time::Duration};
371
372 use futures_util::StreamExt;
373 use polyoxide_rt::{default_runtime, BoxFuture, Instant, Runtime};
374
375 use super::*;
376 use crate::{
377 test_server::{Script, ScriptedServer},
378 tungstenite::{client::IntoClientRequest, Message},
379 Kind,
380 };
381
382 #[derive(Debug)]
384 struct Elapsed;
385
386 impl Runtime for Elapsed {
387 fn spawn(&self, _task: BoxFuture<()>) {}
388
389 fn sleep(&self, _duration: Duration) -> BoxFuture<()> {
390 Box::pin(std::future::ready(()))
391 }
392
393 fn now(&self) -> Instant {
394 Instant::now()
395 }
396 }
397
398 #[test]
399 fn capabilities_are_what_they_were_built_with() {
400 let caps = Capabilities::new(false, true, false);
401 assert!(!caps.client_pings);
402 assert!(caps.sees_server_pings);
403 assert!(!caps.handshake_headers);
404 assert_eq!(Capabilities::FULL, Capabilities::new(true, true, true));
405 assert_ne!(Capabilities::FULL, caps);
406 }
407
408 #[test]
409 fn the_native_default_transport_can_do_everything() {
410 assert_eq!(
411 default_socket_transport().capabilities(),
412 Capabilities::FULL
413 );
414 assert_eq!(
415 TungsteniteTransport::new().capabilities(),
416 Capabilities::FULL
417 );
418 }
419
420 #[tokio::test]
421 async fn the_plain_transport_dials_and_the_socket_reads_a_frame() {
422 let server = ScriptedServer::start(vec![Script::frames(&["hello"])]).await;
423 let request = server.url.as_str().into_client_request().unwrap();
424 let mut socket = TungsteniteTransport::new()
425 .connect(request, Duration::from_secs(2), default_runtime())
426 .await
427 .expect("connect");
428 let first = tokio::time::timeout(Duration::from_secs(3), socket.next())
429 .await
430 .expect("a frame within the window")
431 .expect("the stream has not ended")
432 .expect("a frame, not an error");
433 assert_eq!(first, Message::Text("hello".into()));
434 }
435
436 #[tokio::test]
437 async fn a_refused_upgrade_keeps_its_status() {
438 let server = ScriptedServer::start(vec![Script::refuse_upgrade(503)]).await;
439 let request = server.url.as_str().into_client_request().unwrap();
440 let Err(err) = TungsteniteTransport::new()
441 .connect(request, Duration::from_secs(2), default_runtime())
442 .await
443 else {
444 panic!("a refused upgrade must not connect");
445 };
446 assert_eq!(
447 err.kind(),
448 Kind::Refused {
449 status: 503,
450 retry_after: None
451 }
452 );
453 }
454
455 #[tokio::test]
456 async fn the_transport_bounds_its_connect_with_the_runtime_it_is_given() {
457 let server = ScriptedServer::start(vec![Script::stall()]).await;
460 let request = server.url.as_str().into_client_request().unwrap();
461 let after = Duration::from_secs(3600);
462 let Err(err) = TungsteniteTransport::new()
463 .connect(request, after, Arc::new(Elapsed))
464 .await
465 else {
466 panic!("a stalled handshake must not connect");
467 };
468 assert_eq!(err.kind(), Kind::ConnectTimeout { after });
469 }
470
471 #[test]
472 fn the_setters_write_the_builders_fields() {
473 #[derive(Default)]
474 struct Builder {
475 runtime: Option<Arc<dyn Runtime>>,
476 socket_transport: Option<Arc<dyn SocketTransport>>,
477 }
478
479 impl Builder {
480 crate::transport_setters! {
481 runtime;
483 socket_transport;
485 }
486 }
487
488 let bare = Builder::default();
489 assert!(bare.runtime.is_none() && bare.socket_transport.is_none());
490 let set = Builder::default()
491 .runtime(default_runtime())
492 .socket_transport(default_socket_transport());
493 assert!(set.runtime.is_some() && set.socket_transport.is_some());
494 }
495}
496
497#[cfg(all(test, not(target_arch = "wasm32")))]
498mod connect_tests {
499 use std::{net::SocketAddr, time::Duration};
500
501 use futures_util::StreamExt;
502 use polyoxide_rt::default_runtime;
503
504 use super::*;
505 use crate::{
506 test_server::{Script, ScriptedServer},
507 tungstenite::client::IntoClientRequest,
508 Kind,
509 };
510
511 fn addr(s: &str) -> SocketAddr {
512 s.parse().unwrap()
513 }
514
515 #[test]
516 fn address_families_are_interleaved() {
517 let addrs = vec![
520 addr("[2001:db8::1]:443"),
521 addr("[2001:db8::2]:443"),
522 addr("192.0.2.1:443"),
523 addr("192.0.2.2:443"),
524 addr("192.0.2.3:443"),
525 ];
526 let out = interleave_address_families(addrs);
527 assert_eq!(
528 out,
529 vec![
530 addr("[2001:db8::1]:443"),
531 addr("192.0.2.1:443"),
532 addr("[2001:db8::2]:443"),
533 addr("192.0.2.2:443"),
534 addr("192.0.2.3:443"),
535 ]
536 );
537 }
538
539 #[test]
540 fn single_family_lists_pass_through_unchanged() {
541 let v4 = vec![addr("192.0.2.1:443"), addr("192.0.2.2:443")];
542 assert_eq!(interleave_address_families(v4.clone()), v4);
543 assert!(interleave_address_families(Vec::new()).is_empty());
544 }
545
546 #[test]
547 fn a_uri_without_a_port_dials_its_schemes_default() {
548 for (uri, port) in [
549 ("ws://127.0.0.1/", 80),
550 ("wss://127.0.0.1/", 443),
551 ("ws://127.0.0.1:9/", 9),
552 ("wss://example.com/ws/market", 443),
553 ] {
554 let parsed: Uri = uri.parse().unwrap();
555 let (_, dialled) = dial_target(&parsed).expect(uri);
556 assert_eq!(dialled, port, "{uri}");
557 }
558 assert_eq!(
559 dial_target(&"ws://[::1]/".parse().unwrap()),
560 Some((Target::Ip(std::net::Ipv6Addr::LOCALHOST.into()), 80))
561 );
562 assert_eq!(
563 dial_target(&"wss://example.com/x".parse().unwrap()),
564 Some((Target::Name("example.com"), 443))
565 );
566 }
567
568 #[test]
569 fn a_dns_miss_is_an_io_failure_and_says_what_it_could_not_resolve() {
570 let err = no_addresses(
571 &"wss://ws.example.invalid/feed"
572 .parse()
573 .expect("a valid uri"),
574 );
575 assert_eq!(err.kind(), Kind::Io);
576 assert_eq!(
577 err.to_string(),
578 "IO error: no addresses resolved for wss://ws.example.invalid/feed"
579 );
580 }
581
582 #[tokio::test]
583 async fn per_address_returns_the_upgraded_socket_with_nothing_read() {
584 let server = ScriptedServer::start(vec![Script::frames(&["hello"])]).await;
585 let request = server.url.as_str().into_client_request().unwrap();
586 let mut socket = TungsteniteTransport::per_address()
587 .connect(request, Duration::from_secs(2), default_runtime())
588 .await
589 .expect("connect");
590 let first = tokio::time::timeout(Duration::from_secs(3), socket.next())
591 .await
592 .expect("a frame within the window")
593 .expect("the stream has not ended")
594 .expect("a frame, not an error");
595 assert_eq!(first, Message::Text("hello".into()));
596 }
597
598 #[tokio::test]
599 async fn a_refused_upgrade_is_returned_at_once_with_other_addresses_left() {
600 let server = ScriptedServer::start(vec![Script::refuse_upgrade(503)]).await;
601 let request = server.url.as_str().into_client_request().unwrap();
602 let addr: SocketAddr = server
603 .url
604 .trim_start_matches("ws://")
605 .parse()
606 .expect("the server's address");
607 let Err(err) = dial_addresses(
608 request,
609 vec![addr, addr],
610 Duration::from_secs(2),
611 default_runtime().as_ref(),
612 )
613 .await
614 else {
615 panic!("a refused upgrade must not connect");
616 };
617 assert_eq!(
618 err.kind(),
619 Kind::Refused {
620 status: 503,
621 retry_after: None
622 }
623 );
624 assert_eq!(
625 server.connection_count(),
626 1,
627 "the second address is not tried"
628 );
629 }
630
631 #[tokio::test]
632 async fn a_stalled_address_falls_through_to_the_next() {
633 let stalled = ScriptedServer::start(vec![Script::stall()]).await;
634 let serving = ScriptedServer::start(vec![Script::frames(&["hello"])]).await;
635 let address = |server: &ScriptedServer| -> SocketAddr {
636 server
637 .url
638 .trim_start_matches("ws://")
639 .parse()
640 .expect("the server's address")
641 };
642 let request = serving.url.as_str().into_client_request().unwrap();
643 let mut socket = dial_addresses(
644 request,
645 vec![address(&stalled), address(&serving)],
646 Duration::from_millis(200),
647 default_runtime().as_ref(),
648 )
649 .await
650 .expect("the second address answers");
651 let first = tokio::time::timeout(Duration::from_secs(3), socket.next())
652 .await
653 .expect("a frame within the window")
654 .expect("the stream has not ended")
655 .expect("a frame, not an error");
656 assert_eq!(first, Message::Text("hello".into()));
657 assert_eq!(stalled.connection_count(), 1);
658 }
659
660 #[tokio::test]
661 async fn per_address_bounds_the_handshake_by_the_timeout() {
662 let server = ScriptedServer::start(vec![Script::stall()]).await;
663 let request = server.url.as_str().into_client_request().unwrap();
664 let after = Duration::from_millis(200);
665 let Err(err) = TungsteniteTransport::per_address()
666 .connect(request, after, default_runtime())
667 .await
668 else {
669 panic!("a stalled handshake must not connect");
670 };
671 assert_eq!(err.kind(), Kind::ConnectTimeout { after });
672 }
673
674 #[tokio::test]
675 async fn a_request_naming_no_host_is_a_url_failure_before_any_dial() {
676 let request = Request::builder().uri("/feed").body(()).unwrap();
677 let Err(err) = TungsteniteTransport::per_address()
678 .connect(request, Duration::from_secs(2), default_runtime())
679 .await
680 else {
681 panic!("a request with no host must not connect");
682 };
683 assert_eq!(err.kind(), Kind::Url);
684 }
685}