1//! The HTTPS adapter behind `jev-client`'s ports: one HTTP/2 connection 2//! per backend over rustls, driven by the executor, and a `Runtime` on 3//! the executor's timers. It sends and classifies; retrying, budgets and 4//! statuses are the client's (contract §2). 5 6use std::cell::RefCell; 7use std::rc::Rc; 8use std::task::Waker; 9use std::future::Future; 10use std::net::{IpAddr, SocketAddr}; 11use std::pin::Pin; 12use std::sync::Arc; 13use std::sync::atomic::{AtomicU64, Ordering}; 14use std::time::{Duration, Instant, SystemTime}; 15 16use bytes::Bytes; 17use futures_rustls::TlsConnector; 18use http_body_util::{BodyExt, Full, LengthLimitError, Limited}; 19use hyper::client::conn::http2::SendRequest; 20use jev_client::{HttpRequest, HttpResponse, Runtime, Transport, TransportError, TransportErrorKind}; 21use rustls::pki_types::pem::PemObject; 22use rustls::pki_types::{CertificateDer, ServerName}; 23use rustls::{ClientConfig, RootCertStore}; 24 25use crate::Error; 26use crate::dns::{self, Servers}; 27use crate::executor::{self, random_unit}; 28use crate::net::{HyperIo, PeerSettings, TcpIo}; 29use crate::ratelimit::RateLimit; 30 31/// Cloudflare closes idle connections at 400 s; reconnect well before. 32const IDLE_LIMIT: Duration = Duration::from_secs(300); 33/// The response cap (pg_typesafe's 8 MB). 34const MAX_BODY: usize = 8 << 20; 35/// Time for each address but the last, so one blackholed address (a 36/// broken IPv6 route, a dead IP) cannot spend a whole attempt. 37const CONNECT_PER_ADDRESS: Duration = Duration::from_secs(2); 38/// Streams assumed before a peer has said: RFC 9113 §6.5.2 recommends 39/// no fewer than 100, and api.typesafe.ai advertises 100 (contract §2). 40const ASSUMED_STREAMS: u32 = 100; 41/// No limit seen yet on this connection. 42const UNKNOWN: u64 = u64::MAX; 43 44/// Where requests go: the URL, how its host is reached, which CA file 45/// (if any) is trusted besides the Mozilla roots, and how the connection 46/// is kept alive. A connection is reused only for an equal endpoint, so 47/// changing any of these opens a new one. 48#[derive(Clone, PartialEq)] 49pub struct Endpoint { 50 uri: http::Uri, 51 host: Host, 52 port: u16, 53 server_name: ServerName<'static>, 54 ca_file: Option<String>, 55 keep_alive: KeepAlive, 56} 57 58/// A ping after `interval` without a frame from the server, while a scan 59/// runs the executor; the connection is closed if one goes unanswered for 60/// `timeout` (contract §2: 20 s and 10 s). 61#[derive(Clone, Copy, PartialEq, Debug)] 62pub struct KeepAlive { 63 pub interval: Duration, 64 pub timeout: Duration, 65} 66 67impl Default for KeepAlive { 68 fn default() -> Self { 69 KeepAlive { interval: Duration::from_secs(20), timeout: Duration::from_secs(10) } 70 } 71} 72 73#[derive(Clone, PartialEq)] 74enum Host { 75 Ip(IpAddr), 76 Name { name: String, servers: Servers }, 77} 78 79impl Endpoint { 80 pub fn parse(url: &str, ca_file: Option<&str>, dns_servers: Option<&str>) -> Result<Self, Error> { 81 let config = |e: String| Error::Config(e); 82 let uri: http::Uri = url.parse().map_err(|e| config(format!("jev.endpoint {url}: {e}")))?; 83 if uri.scheme() != Some(&http::uri::Scheme::HTTPS) { 84 return Err(config(format!("jev.endpoint must be https: {url}"))); 85 } 86 let host = uri.host().ok_or_else(|| config(format!("jev.endpoint has no host: {url}")))?; 87 let (host, server_name) = match host.trim_start_matches('[').trim_end_matches(']').parse::<IpAddr>() { 88 Ok(ip) => (Host::Ip(ip), ServerName::IpAddress(ip.into())), 89 Err(_) => ( 90 Host::Name { name: host.to_owned(), servers: Servers::parse(dns_servers)? }, 91 ServerName::try_from(host.to_owned()) 92 .map_err(|e| config(format!("jev.endpoint host {host}: {e}")))?, 93 ), 94 }; 95 Ok(Endpoint { 96 port: uri.port_u16().unwrap_or(443), 97 host, 98 server_name, 99 ca_file: ca_file.map(str::to_owned), 100 keep_alive: KeepAlive::default(), 101 uri, 102 }) 103 } 104 105 pub fn with_keep_alive(mut self, keep_alive: KeepAlive) -> Self { 106 self.keep_alive = keep_alive; 107 self 108 } 109 110 /// Every address to try, in the resolver's order (IPv4 first). 111 async fn addrs(&self) -> Result<Vec<SocketAddr>, TransportError> { 112 let ips = match &self.host { 113 Host::Ip(ip) => vec![*ip], 114 Host::Name { name, servers } => dns::resolve(name, servers).await?, 115 }; 116 Ok(ips.into_iter().map(|ip| SocketAddr::new(ip, self.port)).collect()) 117 } 118} 119 120/// The `Transport` port for one endpoint. 121pub struct HttpsTransport { 122 endpoint: Endpoint, 123 limit: Option<RateLimit>, 124} 125 126impl HttpsTransport { 127 pub fn new(endpoint: Endpoint) -> Self { 128 HttpsTransport { endpoint, limit: None } 129 } 130 131 /// Every attempt first waits for the cluster's rate limit, and its 132 /// outcome feeds the limit back (429s halve it, answers restore it 133 /// and correct the token estimate). 134 pub fn rate_limited(mut self, limit: RateLimit) -> Self { 135 self.limit = Some(limit); 136 self 137 } 138} 139 140impl Transport for HttpsTransport { 141 async fn admit(&self, request: &HttpRequest) { 142 if let Some(limit) = &self.limit { 143 limit.admit(&request.body).await; 144 } 145 } 146 147 async fn send(&self, request: HttpRequest) -> Result<HttpResponse, TransportError> { 148 let limit = self.limit; 149 let sent = request.body.clone(); 150 let result = self.exchange(request).await; 151 if let Some(limit) = limit { 152 match &result { 153 Ok(response) => limit.settle(&sent, response.status, &response.body), 154 Err(e) if e.kind == TransportErrorKind::NotSent => limit.unsent(&sent), 155 Err(_) => {} 156 } 157 } 158 result 159 } 160 161 fn distrust_connection(&self) { 162 forget_connection(); 163 } 164} 165 166impl HttpsTransport { 167 async fn exchange(&self, request: HttpRequest) -> Result<HttpResponse, TransportError> { 168 let mut sender = connection(&self.endpoint).await?; 169 let mut builder = http::Request::post(self.endpoint.uri.clone()); 170 *builder.headers_mut().expect("a fresh builder has headers") = request.headers; 171 let request = builder 172 .body(Full::new(request.body)) 173 .map_err(|e| TransportError::new(TransportErrorKind::Config, format!("building the request: {e}")))?; 174 175 // From here until the body is read, or this future is dropped. 176 let _stream = crate::stats::InFlight::open(); 177 let response = match sender.try_send_request(request).await { 178 Ok(response) => response, 179 Err(mut e) => { 180 forget_connection(); 181 // hyper hands the request back when it never left. 182 let kind = if e.take_message().is_some() || never_processed(e.error()) { 183 TransportErrorKind::NotSent 184 } else { 185 TransportErrorKind::Interrupted 186 }; 187 return Err(TransportError::new(kind, format!("sending to {}: {}", self.endpoint.uri, e.error()))); 188 } 189 }; 190 let (parts, body) = response.into_parts(); 191 let body = match Limited::new(body, MAX_BODY).collect().await { 192 Ok(collected) => collected.to_bytes(), 193 Err(e) if e.is::<LengthLimitError>() => { 194 return Err(TransportError::new( 195 TransportErrorKind::TooLarge, 196 format!("Jev's response exceeded {} MiB", MAX_BODY >> 20), 197 )); 198 } 199 Err(e) => { 200 forget_connection(); 201 return Err(TransportError::new(TransportErrorKind::Interrupted, format!("reading Jev's response: {e}"))); 202 } 203 }; 204 touch_connection(); 205 Ok(HttpResponse { status: parts.status.as_u16(), headers: parts.headers, body }) 206 } 207} 208 209/// True when h2 says the server never processed the stream: refused 210/// (REFUSED_STREAM), or cut by a GOAWAY whose last stream id is below 211/// ours. Such a request can be resent without being billed twice. 212fn never_processed(error: &hyper::Error) -> bool { 213 let mut source: Option<&(dyn std::error::Error + 'static)> = std::error::Error::source(error); 214 while let Some(e) = source { 215 if let Some(h2) = e.downcast_ref::<h2::Error>() { 216 return h2.reason() == Some(h2::Reason::REFUSED_STREAM) || (h2.is_go_away() && h2.is_remote()); 217 } 218 source = e.source(); 219 } 220 false 221} 222 223/// The `Runtime` port on the executor: its timers and Postgres's PRNG. 224pub struct Backend; 225 226impl Runtime for Backend { 227 fn now(&self) -> Instant { 228 Instant::now() 229 } 230 231 fn wall_clock(&self) -> SystemTime { 232 SystemTime::now() 233 } 234 235 fn sleep(&self, duration: Duration) -> impl Future<Output = ()> { 236 executor::sleep(duration) 237 } 238 239 fn timeout<F: Future>(&self, duration: Duration, future: F) -> impl Future<Output = Option<F::Output>> { 240 executor::timeout(duration, future) 241 } 242 243 fn jitter(&self) -> f64 { 244 random_unit() 245 } 246} 247 248struct Connection { 249 endpoint: Endpoint, 250 sender: SendRequest<Full<Bytes>>, 251 last_used: Instant, 252} 253 254thread_local! { 255 static CONNECTION: RefCell<Option<Connection>> = const { RefCell::new(None) }; 256 static DIALING: RefCell<Option<Rc<Dial>>> = const { RefCell::new(None) }; 257 /// Keyed by the CA file's path and modification time, so a rotated 258 /// file is picked up by the next connection (review #16). 259 static TLS: RefCell<Option<(CaSource, Arc<ClientConfig>)>> = const { RefCell::new(None) }; 260 /// The newest connection's `SETTINGS_MAX_CONCURRENT_STREAMS`, written 261 /// by its reader as frames arrive. It outlives the connection, so a 262 /// reconnect to the same endpoint starts from the last value seen. 263 static PEER_STREAMS: RefCell<Option<(Endpoint, Arc<AtomicU64>)>> = const { RefCell::new(None) }; 264} 265 266/// How many streams the peer at `endpoint` accepts at once: the last 267/// `SETTINGS_MAX_CONCURRENT_STREAMS` it sent, or [`ASSUMED_STREAMS`] 268/// before any. hyper queues streams past the limit without saying so, so 269/// `jev.concurrency` is clamped to this (contract §2). 270pub fn stream_limit(endpoint: &Endpoint) -> usize { 271 PEER_STREAMS.with(|p| match p.borrow().as_ref() { 272 Some((seen, max)) if seen == endpoint => match max.load(Ordering::Relaxed) { 273 UNKNOWN => ASSUMED_STREAMS, 274 n => n as u32, 275 }, 276 _ => ASSUMED_STREAMS, 277 }) as usize 278} 279 280/// A handle on the backend's connection. The connection stays cached 281/// while requests run on it, so a cancelled request costs only its 282/// stream; it is replaced when closed, idle past the limit, or distrusted. 283async fn connection(endpoint: &Endpoint) -> Result<SendRequest<Full<Bytes>>, TransportError> { 284 // Between statements nothing polls hyper's connection driver, so a 285 // GOAWAY or a close the server sent meanwhile is still unread and 286 // `is_closed` would say the connection is fine. Two passes: one 287 // wakes the driver on the readable socket, the next lets it run. 288 if CONNECTION.with(|c| c.borrow().is_some()) { 289 executor::yield_now().await; 290 executor::yield_now().await; 291 } 292 let cached = CONNECTION.with(|c| { 293 let mut c = c.borrow_mut(); 294 match c.as_mut() { 295 Some(conn) 296 if conn.endpoint == *endpoint 297 && conn.last_used.elapsed() < IDLE_LIMIT 298 && !conn.sender.is_closed() => 299 { 300 conn.last_used = Instant::now(); 301 Some(conn.sender.clone()) 302 } 303 _ => { 304 *c = None; 305 None 306 } 307 } 308 }); 309 if let Some(sender) = cached { 310 return Ok(sender); 311 } 312 // One dial at a time: a scan starts a window of requests at once, and 313 // each must not open its own connection. 314 let joined = DIALING.with(|d| d.borrow().as_ref().filter(|dial| dial.endpoint == *endpoint).cloned()); 315 if let Some(dial) = joined { 316 return std::future::poll_fn(|cx| match dial.result.borrow().as_ref() { 317 Some(result) => std::task::Poll::Ready(result.clone()), 318 None => { 319 dial.waiters.borrow_mut().push(cx.waker().clone()); 320 std::task::Poll::Pending 321 } 322 }) 323 .await; 324 } 325 let dial = Rc::new(Dial { endpoint: endpoint.clone(), result: RefCell::new(None), waiters: RefCell::new(Vec::new()) }); 326 DIALING.with(|d| *d.borrow_mut() = Some(dial.clone())); 327 let mut publish = Publish(Some(dial)); 328 let result = connect(endpoint).await; 329 if let Ok(sender) = &result { 330 crate::stats::connected(&endpoint.uri); 331 CONNECTION.with(|c| { 332 *c.borrow_mut() = 333 Some(Connection { endpoint: endpoint.clone(), sender: sender.clone(), last_used: Instant::now() }) 334 }); 335 } 336 publish.finish(result.clone()); 337 result 338} 339 340/// A dial in progress, which later senders wait on. 341struct Dial { 342 endpoint: Endpoint, 343 result: RefCell<Option<Result<SendRequest<Full<Bytes>>, TransportError>>>, 344 waiters: RefCell<Vec<Waker>>, 345} 346 347/// Publishes the dial's outcome to its waiters, exactly once. Dropped 348/// unfinished (the dialing request was cancelled), it tells them nothing 349/// was sent, so each resends and one of them dials again. 350struct Publish(Option<Rc<Dial>>); 351 352impl Publish { 353 fn finish(&mut self, result: Result<SendRequest<Full<Bytes>>, TransportError>) { 354 let Some(dial) = self.0.take() else { return }; 355 DIALING.with(|d| { 356 let mut d = d.borrow_mut(); 357 if d.as_ref().is_some_and(|current| Rc::ptr_eq(current, &dial)) { 358 *d = None; 359 } 360 }); 361 *dial.result.borrow_mut() = Some(result); 362 for waker in dial.waiters.borrow_mut().drain(..) { 363 waker.wake(); 364 } 365 } 366} 367 368impl Drop for Publish { 369 fn drop(&mut self) { 370 // try_with: this may run during thread-local teardown. 371 if self.0.is_some() && DIALING.try_with(|_| ()).is_ok() { 372 self.finish(Err(TransportError::new(TransportErrorKind::NotSent, "the connection was being opened by a cancelled request"))); 373 } 374 } 375} 376 377fn forget_connection() { 378 CONNECTION.with(|c| *c.borrow_mut() = None); 379} 380 381fn touch_connection() { 382 CONNECTION.with(|c| { 383 if let Some(conn) = c.borrow_mut().as_mut() { 384 conn.last_used = Instant::now(); 385 } 386 }); 387} 388 389async fn connect(endpoint: &Endpoint) -> Result<SendRequest<Full<Bytes>>, TransportError> { 390 use TransportErrorKind::{Config, Connect, Tls}; 391 let config = tls_config(endpoint.ca_file.as_deref()).map_err(|e| TransportError::new(Config, e.to_string()))?; 392 393 let addrs = endpoint.addrs().await?; 394 let mut failures = Vec::new(); 395 let mut tcp = None; 396 for (i, addr) in addrs.iter().enumerate() { 397 let attempt = TcpIo::connect(*addr); 398 let result = if i + 1 < addrs.len() { 399 executor::timeout(CONNECT_PER_ADDRESS, attempt).await.unwrap_or_else(|| { 400 Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "no answer within 2 s")) 401 }) 402 } else { 403 attempt.await 404 }; 405 match result { 406 Ok(io) => { 407 tcp = Some(io); 408 break; 409 } 410 Err(e) => failures.push(format!("{addr}: {e}")), 411 } 412 } 413 let tcp = tcp.ok_or_else(|| { 414 TransportError::new(Connect, format!("connecting to {}: {}", endpoint.uri, failures.join("; "))) 415 })?; 416 417 let tls = TlsConnector::from(config).connect(endpoint.server_name.clone(), tcp).await.map_err(|e| { 418 // rustls refusing (a certificate, a protocol alert) will refuse 419 // again; an I/O failure mid-handshake is the network. 420 let refused = e.get_ref().is_some_and(|inner| inner.is::<rustls::Error>()); 421 TransportError::new(if refused { Tls } else { Connect }, format!("TLS with {}: {e}", endpoint.uri)) 422 })?; 423 if tls.get_ref().1.alpn_protocol() != Some(b"h2") { 424 return Err(TransportError::new(Tls, format!("{} did not negotiate HTTP/2", endpoint.uri))); 425 } 426 let max_streams = PEER_STREAMS.with(|p| { 427 let seed = p.borrow().as_ref().filter(|(seen, _)| seen == endpoint).map_or(UNKNOWN, |(_, m)| m.load(Ordering::Relaxed)); 428 let max = Arc::new(AtomicU64::new(seed)); 429 *p.borrow_mut() = Some((endpoint.clone(), max.clone())); 430 max 431 }); 432 let tls = PeerSettings::new(tls, move |n| max_streams.store(u64::from(n), Ordering::Relaxed)); 433 // hyper's fixed receive windows (5 MB per connection, 2 MB per 434 // stream) are below 8 MB answers times the scan's window; BDP-based 435 // adaptive windows grow to what the path carries (contract §2). 436 let (sender, connection) = hyper::client::conn::http2::Builder::new(LocalExec) 437 .adaptive_window(true) 438 // Pings find a dead path before the attempt timeout would, and 439 // keep a long scan's quiet connection open (contract §2). They 440 // run only while a scan polls the executor. 441 .timer(BackendTimer) 442 .keep_alive_interval(endpoint.keep_alive.interval) 443 .keep_alive_timeout(endpoint.keep_alive.timeout) 444 .keep_alive_while_idle(true) 445 .handshake(HyperIo(tls)) 446 .await 447 .map_err(|e| TransportError::new(Connect, format!("HTTP/2 with {}: {e}", endpoint.uri)))?; 448 executor::spawn_local(async move { 449 let _ = connection.await; 450 }); 451 Ok(sender) 452} 453 454/// Which extra CA file, as last seen on disk. 455#[derive(Clone, PartialEq)] 456struct CaSource { 457 path: Option<String>, 458 modified: Option<SystemTime>, 459} 460 461/// Built on first use, not in `_PG_init`, and rebuilt when `jev.ca_file` 462/// names another file or the file changes. 463fn tls_config(ca_file: Option<&str>) -> Result<Arc<ClientConfig>, Error> { 464 let source = CaSource { 465 path: ca_file.map(str::to_owned), 466 modified: ca_file.and_then(|p| std::fs::metadata(p).and_then(|m| m.modified()).ok()), 467 }; 468 if let Some(config) = 469 TLS.with(|t| t.borrow().as_ref().filter(|(s, _)| *s == source).map(|(_, c)| c.clone())) 470 { 471 return Ok(config); 472 } 473 let mut roots = RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; 474 if let Some(path) = ca_file { 475 for cert in CertificateDer::pem_file_iter(path).map_err(|e| Error::Config(format!("jev.ca_file {path}: {e}")))? { 476 let cert = cert.map_err(|e| Error::Config(format!("jev.ca_file {path}: {e}")))?; 477 roots.add(cert).map_err(|e| Error::Config(format!("jev.ca_file {path}: {e}")))?; 478 } 479 } 480 let mut config = ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) 481 .with_safe_default_protocol_versions() 482 .map_err(|e| Error::Config(e.to_string()))? 483 .with_root_certificates(roots) 484 .with_no_client_auth(); 485 config.alpn_protocols = vec![b"h2".to_vec()]; 486 let config = Arc::new(config); 487 TLS.with(|t| *t.borrow_mut() = Some((source, config.clone()))); 488 Ok(config) 489} 490 491/// hyper's timers (keep-alive pings) on the executor's. 492#[derive(Clone, Copy)] 493struct BackendTimer; 494 495impl hyper::rt::Timer for BackendTimer { 496 fn sleep(&self, duration: Duration) -> Pin<Box<dyn hyper::rt::Sleep>> { 497 Box::pin(executor::sleep(duration)) 498 } 499 500 fn sleep_until(&self, deadline: Instant) -> Pin<Box<dyn hyper::rt::Sleep>> { 501 Box::pin(executor::sleep_until(deadline)) 502 } 503} 504 505// Send + Sync: a deadline and a timer id; it is polled only on the 506// backend thread, where its timer lives. 507impl hyper::rt::Sleep for executor::Sleep {} 508 509/// hyper spawns its connection driver here. 510#[derive(Clone, Copy)] 511struct LocalExec; 512 513impl<F: Future<Output = ()> + 'static> hyper::rt::Executor<F> for LocalExec { 514 fn execute(&self, future: F) { 515 executor::spawn_local(future); 516 } 517}