This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / bobbin / crates / runtime / src / network.rs
11 kB 334 lines
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}