This repository has no description
1use std::future::Future;
2use std::net::SocketAddr;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use bytes::Bytes;
7use futures::stream::{Stream, StreamExt};
8use http::{HeaderMap, StatusCode};
9use thiserror::Error;
10use tokio_tungstenite::tungstenite::{
11 Bytes as WsBytes, Message as TungsteniteMessage, protocol::CloseFrame as TungsteniteClose,
12 protocol::frame::coding::CloseCode as TungsteniteCloseCode,
13};
14use url::Url;
15
16#[derive(Debug, Error)]
17pub enum NetworkError {
18 #[error("connect: {0}")]
19 Connect(String),
20 #[error("timeout: {0}")]
21 Timeout(String),
22 #[error("redirect: {0}")]
23 Redirect(String),
24 #[error("transport: {0}")]
25 Transport(String),
26 #[error("body: {0}")]
27 Body(String),
28 #[error("protocol: {0}")]
29 Protocol(String),
30}
31
32pub struct HttpRequest {
33 pub url: Url,
34 pub headers: HeaderMap,
35}
36
37pub type BodyStream = Pin<Box<dyn Stream<Item = Result<Bytes, NetworkError>> + Send + 'static>>;
38
39pub struct HttpResponseHead {
40 pub status: StatusCode,
41 pub headers: HeaderMap,
42 pub content_length: Option<u64>,
43 pub body: BodyStream,
44}
45
46pub type HttpResult = Result<HttpResponseHead, NetworkError>;
47pub type HttpResponseFuture = Pin<Box<dyn Future<Output = HttpResult> + Send + 'static>>;
48
49pub trait HttpTransport: Send + Sync + 'static {
50 fn execute(&self, request: HttpRequest) -> HttpResponseFuture;
51}
52
53#[derive(Clone, Debug)]
54pub struct ReqwestHttp {
55 client: reqwest::Client,
56}
57
58impl ReqwestHttp {
59 pub fn new(client: reqwest::Client) -> Self {
60 Self { client }
61 }
62
63 pub fn shared(client: reqwest::Client) -> Arc<dyn HttpTransport> {
64 Arc::new(Self::new(client))
65 }
66}
67
68#[derive(Debug, Error)]
69pub enum ExtraCaError {
70 #[error("read {path}: {source}")]
71 Read {
72 path: String,
73 source: std::io::Error,
74 },
75 #[error("parse PEM certificate at {path}: {source}")]
76 Parse { path: String, source: reqwest::Error },
77}
78
79/// Loads a PEM certificate from `path` (if given) so it can be added to a
80/// `reqwest::ClientBuilder` via `add_root_certificate`, extending the bundled
81/// webpki roots rather than replacing them. Used to trust a local dev CA -
82/// `rustls-tls-webpki-roots` ignores the OS trust store, so mounting a CA
83/// into `/etc/ssl/certs` has no effect on reqwest without this.
84pub fn load_extra_ca_cert(path: &std::path::Path) -> Result<reqwest::Certificate, ExtraCaError> {
85 let pem = std::fs::read(path).map_err(|source| ExtraCaError::Read {
86 path: path.display().to_string(),
87 source,
88 })?;
89 reqwest::Certificate::from_pem(&pem).map_err(|source| ExtraCaError::Parse {
90 path: path.display().to_string(),
91 source,
92 })
93}
94
95impl HttpTransport for ReqwestHttp {
96 fn execute(&self, request: HttpRequest) -> HttpResponseFuture {
97 let client = self.client.clone();
98 Box::pin(async move {
99 let resp = client
100 .get(request.url)
101 .headers(request.headers)
102 .send()
103 .await
104 .map_err(map_reqwest)?;
105 let status = resp.status();
106 let headers = resp.headers().clone();
107 let content_length = resp.content_length();
108 let body: BodyStream = Box::pin(
109 resp.bytes_stream()
110 .map(|chunk| chunk.map_err(|e| NetworkError::Body(e.to_string()))),
111 );
112 Ok(HttpResponseHead {
113 status,
114 headers,
115 content_length,
116 body,
117 })
118 })
119 }
120}
121
122/// Lets jacquard resolve identities over the workspace reqwest (0.13); jacquard's own
123/// `HttpClient` impl is against reqwest 0.12, which is built here without TLS.
124impl jacquard_common::http_client::HttpClient for ReqwestHttp {
125 type Error = reqwest::Error;
126
127 async fn send_http(
128 &self,
129 request: http::Request<Vec<u8>>,
130 ) -> Result<http::Response<Vec<u8>>, reqwest::Error> {
131 let (parts, body) = request.into_parts();
132 let mut req = self
133 .client
134 .request(parts.method, parts.uri.to_string())
135 .body(body);
136 for (name, value) in parts.headers.iter() {
137 req = req.header(name, value);
138 }
139
140 let resp = req.send().await?;
141 let mut builder = http::Response::builder().status(resp.status());
142 for (name, value) in resp.headers().iter() {
143 builder = builder.header(name, value);
144 }
145 let body = resp.bytes().await?.to_vec();
146 Ok(builder.body(body).expect("response parts came from reqwest"))
147 }
148}
149
150fn map_reqwest(err: reqwest::Error) -> NetworkError {
151 let msg = err.to_string();
152 if err.is_timeout() {
153 NetworkError::Timeout(msg)
154 } else if err.is_connect() {
155 NetworkError::Connect(msg)
156 } else if err.is_redirect() {
157 NetworkError::Redirect(msg)
158 } else {
159 NetworkError::Transport(msg)
160 }
161}
162
163#[derive(Clone, Debug)]
164pub enum WsMessage {
165 Text(String),
166 Binary(Bytes),
167 Ping(Bytes),
168 Pong(Bytes),
169 Close { code: u16, reason: String },
170}
171
172pub type WsSendFuture<'a> = Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send + 'a>>;
173pub type WsMessageFuture<'a> =
174 Pin<Box<dyn Future<Output = Option<Result<WsMessage, NetworkError>>> + Send + 'a>>;
175
176pub trait WsSink: Send + 'static {
177 fn send<'a>(&'a mut self, message: WsMessage) -> WsSendFuture<'a>;
178}
179
180pub trait WsStream: Send + 'static {
181 fn next<'a>(&'a mut self) -> WsMessageFuture<'a>;
182}
183
184pub struct WsConn {
185 pub sink: Box<dyn WsSink>,
186 pub stream: Box<dyn WsStream>,
187}
188
189pub type WsConnectFuture =
190 Pin<Box<dyn Future<Output = Result<WsConn, NetworkError>> + Send + 'static>>;
191
192pub trait WsTransport: Send + Sync + 'static {
193 fn connect(&self, url: Url) -> WsConnectFuture;
194}
195
196pub type AddrGuard = Arc<dyn Fn(&[SocketAddr]) -> Result<(), NetworkError> + Send + Sync>;
197
198#[derive(Clone, Copy, Debug, Default)]
199pub struct TungsteniteWs;
200
201impl TungsteniteWs {
202 pub fn shared() -> Arc<dyn WsTransport> {
203 Arc::new(Self)
204 }
205}
206
207impl WsTransport for TungsteniteWs {
208 fn connect(&self, url: Url) -> WsConnectFuture {
209 Box::pin(async move {
210 let url_str = url.as_str().to_owned();
211 let (ws, _resp) = tokio_tungstenite::connect_async(&url_str)
212 .await
213 .map_err(|e| NetworkError::Connect(e.to_string()))?;
214 let (sink_inner, stream_inner) = futures::StreamExt::split(ws);
215 let sink: Box<dyn WsSink> = Box::new(TungsteniteSink { inner: sink_inner });
216 let stream: Box<dyn WsStream> = Box::new(TungsteniteStream {
217 inner: stream_inner,
218 });
219 Ok(WsConn { sink, stream })
220 })
221 }
222}
223
224pub struct GuardedWs {
225 guard: AddrGuard,
226}
227
228impl GuardedWs {
229 pub fn shared(guard: AddrGuard) -> Arc<dyn WsTransport> {
230 Arc::new(Self { guard })
231 }
232}
233
234impl WsTransport for GuardedWs {
235 fn connect(&self, url: Url) -> WsConnectFuture {
236 let guard = self.guard.clone();
237 Box::pin(async move {
238 let host = url
239 .host_str()
240 .ok_or_else(|| NetworkError::Connect("ws url missing host".to_owned()))?
241 .to_owned();
242 let port = url
243 .port_or_known_default()
244 .ok_or_else(|| NetworkError::Connect("ws url missing port".to_owned()))?;
245 let addrs: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), port))
246 .await
247 .map_err(|e| NetworkError::Connect(e.to_string()))?
248 .collect();
249 guard(&addrs)?;
250 let addr = addrs
251 .into_iter()
252 .next()
253 .ok_or_else(|| NetworkError::Connect(format!("no addresses for {host}")))?;
254 let tcp = tokio::net::TcpStream::connect(addr)
255 .await
256 .map_err(|e| NetworkError::Connect(e.to_string()))?;
257 let (ws, _resp) = tokio_tungstenite::client_async_tls(url.as_str(), tcp)
258 .await
259 .map_err(|e| NetworkError::Connect(e.to_string()))?;
260 let (sink_inner, stream_inner) = futures::StreamExt::split(ws);
261 let sink: Box<dyn WsSink> = Box::new(TungsteniteSink { inner: sink_inner });
262 let stream: Box<dyn WsStream> = Box::new(TungsteniteStream {
263 inner: stream_inner,
264 });
265 Ok(WsConn { sink, stream })
266 })
267 }
268}
269
270type TungsteniteWsStream =
271 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
272
273struct TungsteniteSink {
274 inner: futures::stream::SplitSink<TungsteniteWsStream, TungsteniteMessage>,
275}
276
277impl WsSink for TungsteniteSink {
278 fn send<'a>(&'a mut self, message: WsMessage) -> WsSendFuture<'a> {
279 Box::pin(async move {
280 use futures::SinkExt;
281 self.inner
282 .send(message_to_tungstenite(message))
283 .await
284 .map_err(|e| NetworkError::Transport(e.to_string()))
285 })
286 }
287}
288
289struct TungsteniteStream {
290 inner: futures::stream::SplitStream<TungsteniteWsStream>,
291}
292
293impl WsStream for TungsteniteStream {
294 fn next<'a>(&'a mut self) -> WsMessageFuture<'a> {
295 Box::pin(async move {
296 let item = StreamExt::next(&mut self.inner).await?;
297 Some(
298 item.map_err(|e| NetworkError::Transport(e.to_string()))
299 .and_then(message_from_tungstenite),
300 )
301 })
302 }
303}
304
305fn message_to_tungstenite(message: WsMessage) -> TungsteniteMessage {
306 match message {
307 WsMessage::Text(text) => TungsteniteMessage::Text(text.into()),
308 WsMessage::Binary(bytes) => TungsteniteMessage::Binary(WsBytes::copy_from_slice(&bytes)),
309 WsMessage::Ping(bytes) => TungsteniteMessage::Ping(WsBytes::copy_from_slice(&bytes)),
310 WsMessage::Pong(bytes) => TungsteniteMessage::Pong(WsBytes::copy_from_slice(&bytes)),
311 WsMessage::Close { code, reason } => TungsteniteMessage::Close(Some(TungsteniteClose {
312 code: TungsteniteCloseCode::from(code),
313 reason: reason.into(),
314 })),
315 }
316}
317
318fn message_from_tungstenite(message: TungsteniteMessage) -> Result<WsMessage, NetworkError> {
319 match message {
320 TungsteniteMessage::Text(t) => Ok(WsMessage::Text(t.to_string())),
321 TungsteniteMessage::Binary(b) => Ok(WsMessage::Binary(Bytes::copy_from_slice(&b))),
322 TungsteniteMessage::Ping(b) => Ok(WsMessage::Ping(Bytes::copy_from_slice(&b))),
323 TungsteniteMessage::Pong(b) => Ok(WsMessage::Pong(Bytes::copy_from_slice(&b))),
324 TungsteniteMessage::Close(close) => {
325 let (code, reason) = close
326 .map(|c| (u16::from(c.code), c.reason.to_string()))
327 .unwrap_or((1000, String::new()));
328 Ok(WsMessage::Close { code, reason })
329 }
330 TungsteniteMessage::Frame(_) => Err(NetworkError::Protocol(
331 "tungstenite raw frame surfaced unexpectedly".to_owned(),
332 )),
333 }
334}