Skip to main content

tacenta_client/
dial.rs

1//! How a client reaches the four services: TCP (plain or under TLS), or the
2//! WebSocket carriage (decision 0090, step 3). One value, threaded through
3//! every connect and reconnect, so the protocol code never knows which.
4
5use std::future::Future;
6use std::net::SocketAddr;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use tacenta_transport::{
11    AccountConnection, ClientTls, Connection, DirConnection, ProvisionConnection,
12};
13
14/// A byte stream a service can be spoken over.
15pub trait ByteStream: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync {}
16impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync> ByteStream for T {}
17
18/// The future a [`Connector`] returns.
19pub type Connecting = Pin<Box<dyn Future<Output = std::io::Result<Box<dyn ByteStream>>> + Send>>;
20
21/// A caller-supplied way of reaching the four services: given a service
22/// name (`directory`, `relay`, `accounts`, `provisioning`), open a byte
23/// stream to it. This is how a host that owns the sockets, the browser
24/// above all, lends them to the client: the WebAssembly head implements it
25/// with a WebSocket per service opened from JavaScript (decision 0090,
26/// step 4). The protocol spoken over the stream is unchanged.
27pub trait Connector: Send + Sync {
28    fn open(&self, service: &str) -> Connecting;
29}
30
31// On wasm the native arms are compiled out, so their fields go unread there.
32#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
33#[derive(Clone)]
34pub(crate) enum Dialer {
35    /// The service's own TCP port, with the certificate name and trust for
36    /// TLS, or `None` for a plaintext development server.
37    Tcp { trust: Option<(String, ClientTls)> },
38    /// `{base}/{service}` over WebSocket; `wss://` is checked under `tls`
39    /// presenting the URL's host. The addresses the caller passes are
40    /// ignored: the gateway dials the services.
41    WebSocket { base: String, tls: ClientTls },
42    /// Streams the caller opens; the addresses are ignored.
43    Custom(Arc<dyn Connector>),
44}
45
46/// TCP is a native affair; a browser build reaches the services only
47/// through a [`Connector`] or the WebSocket carriage.
48#[cfg(target_arch = "wasm32")]
49fn no_tcp<T>() -> std::io::Result<T> {
50    Err(std::io::Error::new(
51        std::io::ErrorKind::Unsupported,
52        "TCP is not available on this target; use the websocket carriage or a Connector",
53    ))
54}
55
56impl Dialer {
57    pub(crate) fn tcp(trust: Option<(&str, &ClientTls)>) -> Dialer {
58        Dialer::Tcp {
59            trust: trust.map(|(name, tls)| (name.to_owned(), tls.clone())),
60        }
61    }
62
63    #[cfg(not(target_arch = "wasm32"))]
64    fn url(base: &str, service: &str) -> String {
65        format!("{}/{service}", base.trim_end_matches('/'))
66    }
67
68    pub(crate) async fn accounts(&self, addr: SocketAddr) -> std::io::Result<AccountConnection> {
69        match self {
70            Dialer::Custom(connector) => {
71                let stream = connector.open("accounts").await?;
72                AccountConnection::establish(stream).await
73            }
74            #[cfg(not(target_arch = "wasm32"))]
75            Dialer::Tcp {
76                trust: Some((name, tls)),
77            } => AccountConnection::connect_tls(addr, name, tls).await,
78            #[cfg(not(target_arch = "wasm32"))]
79            Dialer::Tcp { trust: None } => AccountConnection::connect(addr).await,
80            #[cfg(not(target_arch = "wasm32"))]
81            Dialer::WebSocket { base, tls } => {
82                AccountConnection::connect_ws(&Dialer::url(base, "accounts"), tls).await
83            }
84            #[cfg(target_arch = "wasm32")]
85            Dialer::Tcp { .. } | Dialer::WebSocket { .. } => {
86                let _ = addr;
87                no_tcp()
88            }
89        }
90    }
91
92    pub(crate) async fn provisioning(
93        &self,
94        addr: SocketAddr,
95    ) -> std::io::Result<ProvisionConnection> {
96        match self {
97            Dialer::Custom(connector) => {
98                let stream = connector.open("provisioning").await?;
99                ProvisionConnection::establish(stream).await
100            }
101            #[cfg(not(target_arch = "wasm32"))]
102            Dialer::Tcp {
103                trust: Some((name, tls)),
104            } => ProvisionConnection::connect_tls(addr, name, tls).await,
105            #[cfg(not(target_arch = "wasm32"))]
106            Dialer::Tcp { trust: None } => ProvisionConnection::connect(addr).await,
107            #[cfg(not(target_arch = "wasm32"))]
108            Dialer::WebSocket { base, tls } => {
109                ProvisionConnection::connect_ws(&Dialer::url(base, "provisioning"), tls).await
110            }
111            #[cfg(target_arch = "wasm32")]
112            Dialer::Tcp { .. } | Dialer::WebSocket { .. } => {
113                let _ = addr;
114                no_tcp()
115            }
116        }
117    }
118
119    pub(crate) async fn directory(&self, addr: SocketAddr) -> std::io::Result<DirConnection> {
120        match self {
121            Dialer::Custom(connector) => {
122                let stream = connector.open("directory").await?;
123                DirConnection::establish(stream).await
124            }
125            #[cfg(not(target_arch = "wasm32"))]
126            Dialer::Tcp {
127                trust: Some((name, tls)),
128            } => DirConnection::connect_tls(addr, name, tls).await,
129            #[cfg(not(target_arch = "wasm32"))]
130            Dialer::Tcp { trust: None } => DirConnection::connect(addr).await,
131            #[cfg(not(target_arch = "wasm32"))]
132            Dialer::WebSocket { base, tls } => {
133                DirConnection::connect_ws(&Dialer::url(base, "directory"), tls).await
134            }
135            #[cfg(target_arch = "wasm32")]
136            Dialer::Tcp { .. } | Dialer::WebSocket { .. } => {
137                let _ = addr;
138                no_tcp()
139            }
140        }
141    }
142
143    pub(crate) async fn relay(
144        &self,
145        addr: SocketAddr,
146        device: &tacenta_relay::DeviceAddr,
147        sign: impl FnOnce(&[u8]) -> Vec<u8>,
148        signal: Arc<tokio::sync::Notify>,
149    ) -> std::io::Result<Connection> {
150        match self {
151            Dialer::Custom(connector) => {
152                let stream = connector.open("relay").await?;
153                Connection::establish_with_signal(stream, device, sign, signal).await
154            }
155            #[cfg(not(target_arch = "wasm32"))]
156            Dialer::Tcp {
157                trust: Some((name, tls)),
158            } => {
159                Connection::connect_as_tls_with_signal(addr, name, tls, device, sign, signal).await
160            }
161            #[cfg(not(target_arch = "wasm32"))]
162            Dialer::Tcp { trust: None } => {
163                Connection::connect_as_with_signal(addr, device, sign, signal).await
164            }
165            #[cfg(not(target_arch = "wasm32"))]
166            Dialer::WebSocket { base, tls } => {
167                Connection::connect_as_ws_with_signal(
168                    &Dialer::url(base, "relay"),
169                    tls,
170                    device,
171                    sign,
172                    signal,
173                )
174                .await
175            }
176            #[cfg(target_arch = "wasm32")]
177            Dialer::Tcp { .. } | Dialer::WebSocket { .. } => {
178                let _ = (addr, device, sign, signal);
179                no_tcp()
180            }
181        }
182    }
183}