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 yield_now(&self) -> BoxFuture<()> {
394 Box::pin(tokio::task::yield_now())
395 }
396
397 fn now(&self) -> Instant {
398 Instant::now()
399 }
400 }
401
402 #[test]
403 fn capabilities_are_what_they_were_built_with() {
404 let caps = Capabilities::new(false, true, false);
405 assert!(!caps.client_pings);
406 assert!(caps.sees_server_pings);
407 assert!(!caps.handshake_headers);
408 assert_eq!(Capabilities::FULL, Capabilities::new(true, true, true));
409 assert_ne!(Capabilities::FULL, caps);
410 }
411
412 #[test]
413 fn the_native_default_transport_can_do_everything() {
414 assert_eq!(
415 default_socket_transport().capabilities(),
416 Capabilities::FULL
417 );
418 assert_eq!(
419 TungsteniteTransport::new().capabilities(),
420 Capabilities::FULL
421 );
422 }
423
424 #[tokio::test]
425 async fn the_plain_transport_dials_and_the_socket_reads_a_frame() {
426 let server = ScriptedServer::start(vec![Script::frames(&["hello"])]).await;
427 let request = server.url.as_str().into_client_request().unwrap();
428 let mut socket = TungsteniteTransport::new()
429 .connect(request, Duration::from_secs(2), default_runtime())
430 .await
431 .expect("connect");
432 let first = tokio::time::timeout(Duration::from_secs(3), socket.next())
433 .await
434 .expect("a frame within the window")
435 .expect("the stream has not ended")
436 .expect("a frame, not an error");
437 assert_eq!(first, Message::Text("hello".into()));
438 }
439
440 #[tokio::test]
441 async fn a_refused_upgrade_keeps_its_status() {
442 let server = ScriptedServer::start(vec![Script::refuse_upgrade(503)]).await;
443 let request = server.url.as_str().into_client_request().unwrap();
444 let Err(err) = TungsteniteTransport::new()
445 .connect(request, Duration::from_secs(2), default_runtime())
446 .await
447 else {
448 panic!("a refused upgrade must not connect");
449 };
450 assert_eq!(
451 err.kind(),
452 Kind::Refused {
453 status: 503,
454 retry_after: None
455 }
456 );
457 }
458
459 #[tokio::test]
460 async fn the_transport_bounds_its_connect_with_the_runtime_it_is_given() {
461 let server = ScriptedServer::start(vec![Script::stall()]).await;
464 let request = server.url.as_str().into_client_request().unwrap();
465 let after = Duration::from_secs(3600);
466 let Err(err) = TungsteniteTransport::new()
467 .connect(request, after, Arc::new(Elapsed))
468 .await
469 else {
470 panic!("a stalled handshake must not connect");
471 };
472 assert_eq!(err.kind(), Kind::ConnectTimeout { after });
473 }
474
475 #[test]
476 fn the_setters_write_the_builders_fields() {
477 #[derive(Default)]
478 struct Builder {
479 runtime: Option<Arc<dyn Runtime>>,
480 socket_transport: Option<Arc<dyn SocketTransport>>,
481 }
482
483 impl Builder {
484 crate::transport_setters! {
485 runtime;
487 socket_transport;
489 }
490 }
491
492 let bare = Builder::default();
493 assert!(bare.runtime.is_none() && bare.socket_transport.is_none());
494 let set = Builder::default()
495 .runtime(default_runtime())
496 .socket_transport(default_socket_transport());
497 assert!(set.runtime.is_some() && set.socket_transport.is_some());
498 }
499}
500
501#[cfg(all(test, not(target_arch = "wasm32")))]
502mod connect_tests {
503 use std::{net::SocketAddr, time::Duration};
504
505 use futures_util::StreamExt;
506 use polyoxide_rt::default_runtime;
507
508 use super::*;
509 use crate::{
510 test_server::{Script, ScriptedServer},
511 tungstenite::client::IntoClientRequest,
512 Kind,
513 };
514
515 fn addr(s: &str) -> SocketAddr {
516 s.parse().unwrap()
517 }
518
519 #[test]
520 fn address_families_are_interleaved() {
521 let addrs = vec![
524 addr("[2001:db8::1]:443"),
525 addr("[2001:db8::2]:443"),
526 addr("192.0.2.1:443"),
527 addr("192.0.2.2:443"),
528 addr("192.0.2.3:443"),
529 ];
530 let out = interleave_address_families(addrs);
531 assert_eq!(
532 out,
533 vec![
534 addr("[2001:db8::1]:443"),
535 addr("192.0.2.1:443"),
536 addr("[2001:db8::2]:443"),
537 addr("192.0.2.2:443"),
538 addr("192.0.2.3:443"),
539 ]
540 );
541 }
542
543 #[test]
544 fn single_family_lists_pass_through_unchanged() {
545 let v4 = vec![addr("192.0.2.1:443"), addr("192.0.2.2:443")];
546 assert_eq!(interleave_address_families(v4.clone()), v4);
547 assert!(interleave_address_families(Vec::new()).is_empty());
548 }
549
550 #[test]
551 fn a_uri_without_a_port_dials_its_schemes_default() {
552 for (uri, port) in [
553 ("ws://127.0.0.1/", 80),
554 ("wss://127.0.0.1/", 443),
555 ("ws://127.0.0.1:9/", 9),
556 ("wss://example.com/ws/market", 443),
557 ] {
558 let parsed: Uri = uri.parse().unwrap();
559 let (_, dialled) = dial_target(&parsed).expect(uri);
560 assert_eq!(dialled, port, "{uri}");
561 }
562 assert_eq!(
563 dial_target(&"ws://[::1]/".parse().unwrap()),
564 Some((Target::Ip(std::net::Ipv6Addr::LOCALHOST.into()), 80))
565 );
566 assert_eq!(
567 dial_target(&"wss://example.com/x".parse().unwrap()),
568 Some((Target::Name("example.com"), 443))
569 );
570 }
571
572 #[test]
573 fn a_dns_miss_is_an_io_failure_and_says_what_it_could_not_resolve() {
574 let err = no_addresses(
575 &"wss://ws.example.invalid/feed"
576 .parse()
577 .expect("a valid uri"),
578 );
579 assert_eq!(err.kind(), Kind::Io);
580 assert_eq!(
581 err.to_string(),
582 "IO error: no addresses resolved for wss://ws.example.invalid/feed"
583 );
584 }
585
586 #[tokio::test]
587 async fn per_address_returns_the_upgraded_socket_with_nothing_read() {
588 let server = ScriptedServer::start(vec![Script::frames(&["hello"])]).await;
589 let request = server.url.as_str().into_client_request().unwrap();
590 let mut socket = TungsteniteTransport::per_address()
591 .connect(request, Duration::from_secs(2), default_runtime())
592 .await
593 .expect("connect");
594 let first = tokio::time::timeout(Duration::from_secs(3), socket.next())
595 .await
596 .expect("a frame within the window")
597 .expect("the stream has not ended")
598 .expect("a frame, not an error");
599 assert_eq!(first, Message::Text("hello".into()));
600 }
601
602 #[tokio::test]
603 async fn a_refused_upgrade_is_returned_at_once_with_other_addresses_left() {
604 let server = ScriptedServer::start(vec![Script::refuse_upgrade(503)]).await;
605 let request = server.url.as_str().into_client_request().unwrap();
606 let addr: SocketAddr = server
607 .url
608 .trim_start_matches("ws://")
609 .parse()
610 .expect("the server's address");
611 let Err(err) = dial_addresses(
612 request,
613 vec![addr, addr],
614 Duration::from_secs(2),
615 default_runtime().as_ref(),
616 )
617 .await
618 else {
619 panic!("a refused upgrade must not connect");
620 };
621 assert_eq!(
622 err.kind(),
623 Kind::Refused {
624 status: 503,
625 retry_after: None
626 }
627 );
628 assert_eq!(
629 server.connection_count(),
630 1,
631 "the second address is not tried"
632 );
633 }
634
635 #[tokio::test]
636 async fn a_stalled_address_falls_through_to_the_next() {
637 let stalled = ScriptedServer::start(vec![Script::stall()]).await;
638 let serving = ScriptedServer::start(vec![Script::frames(&["hello"])]).await;
639 let address = |server: &ScriptedServer| -> SocketAddr {
640 server
641 .url
642 .trim_start_matches("ws://")
643 .parse()
644 .expect("the server's address")
645 };
646 let request = serving.url.as_str().into_client_request().unwrap();
647 let mut socket = dial_addresses(
648 request,
649 vec![address(&stalled), address(&serving)],
650 Duration::from_millis(200),
651 default_runtime().as_ref(),
652 )
653 .await
654 .expect("the second address answers");
655 let first = tokio::time::timeout(Duration::from_secs(3), socket.next())
656 .await
657 .expect("a frame within the window")
658 .expect("the stream has not ended")
659 .expect("a frame, not an error");
660 assert_eq!(first, Message::Text("hello".into()));
661 assert_eq!(stalled.connection_count(), 1);
662 }
663
664 #[tokio::test]
665 async fn per_address_bounds_the_handshake_by_the_timeout() {
666 let server = ScriptedServer::start(vec![Script::stall()]).await;
667 let request = server.url.as_str().into_client_request().unwrap();
668 let after = Duration::from_millis(200);
669 let Err(err) = TungsteniteTransport::per_address()
670 .connect(request, after, default_runtime())
671 .await
672 else {
673 panic!("a stalled handshake must not connect");
674 };
675 assert_eq!(err.kind(), Kind::ConnectTimeout { after });
676 }
677
678 #[tokio::test]
679 async fn a_request_naming_no_host_is_a_url_failure_before_any_dial() {
680 let request = Request::builder().uri("/feed").body(()).unwrap();
681 let Err(err) = TungsteniteTransport::per_address()
682 .connect(request, Duration::from_secs(2), default_runtime())
683 .await
684 else {
685 panic!("a request with no host must not connect");
686 };
687 assert_eq!(err.kind(), Kind::Url);
688 }
689}