1use std::sync::Arc;
30use std::sync::OnceLock;
31use std::time::Duration;
32
33use beck_core::net::{Failure, Outbound, Reply, Request, Stop};
34use http_body_util::{BodyExt, Full, Limited};
35use hyper::body::Bytes;
36use tokio_rustls::rustls::pki_types::ServerName;
37use tokio_rustls::rustls::ClientConfig;
38use tokio_rustls::TlsConnector;
39
40pub const DEFAULT_TIMEOUT_MS: i64 = 10_000;
47
48const POLL_MS: Duration = Duration::from_millis(5);
55
56pub const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
62
63fn client_config() -> &'static Arc<ClientConfig> {
70 static CONFIG: OnceLock<Arc<ClientConfig>> = OnceLock::new();
71 CONFIG.get_or_init(|| {
72 let mut roots = tokio_rustls::rustls::RootCertStore::empty();
73 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
74 Arc::new(config_for(roots))
75 })
76}
77
78pub const TLS_VERSIONS: &[&tokio_rustls::rustls::SupportedProtocolVersion] =
89 &[&tokio_rustls::rustls::version::TLS13];
90
91fn config_for(roots: tokio_rustls::rustls::RootCertStore) -> ClientConfig {
94 let mut config = ClientConfig::builder_with_provider(Arc::new(
95 tokio_rustls::rustls::crypto::aws_lc_rs::default_provider(),
96 ))
97 .with_protocol_versions(TLS_VERSIONS)
98 .expect("TLS 1.3 is supported by the provider")
99 .with_root_certificates(roots)
100 .with_no_client_auth();
101 config.alpn_protocols = vec![b"http/1.1".to_vec()];
104 config
105}
106
107#[derive(Debug)]
108pub struct HttpOutbound {
109 runtime: tokio::runtime::Runtime,
110 timeout: Duration,
111 roots: Option<Arc<ClientConfig>>,
114}
115
116impl HttpOutbound {
117 pub fn new() -> std::io::Result<HttpOutbound> {
118 HttpOutbound::with_timeout(DEFAULT_TIMEOUT_MS)
119 }
120
121 pub fn with_timeout(millis: i64) -> std::io::Result<HttpOutbound> {
122 Ok(HttpOutbound {
123 runtime: tokio::runtime::Builder::new_multi_thread()
124 .worker_threads(1)
125 .enable_all()
126 .thread_name("beck-outbound")
127 .build()?,
128 timeout: Duration::from_millis(millis.max(1) as u64),
129 roots: None,
130 })
131 }
132
133 #[cfg(test)]
140 fn trusting(roots: tokio_rustls::rustls::RootCertStore) -> std::io::Result<HttpOutbound> {
141 Ok(HttpOutbound {
142 roots: Some(Arc::new(config_for(roots))),
143 ..HttpOutbound::new()?
144 })
145 }
146
147 fn tls(&self) -> Arc<ClientConfig> {
148 self.roots
149 .clone()
150 .unwrap_or_else(|| Arc::clone(client_config()))
151 }
152
153 pub fn install() -> bool {
159 match HttpOutbound::new() {
160 Ok(client) => beck_core::net::set_process_outbound(Arc::new(client)),
161 Err(e) => {
162 tracing::warn!(error = %e, "no outbound HTTP client: requests will be refused");
163 false
164 }
165 }
166 }
167}
168
169impl Outbound for HttpOutbound {
170 fn fetch(&self, request: &Request, stop: &Stop) -> Result<Reply, Failure> {
171 let millis = self.timeout.as_millis() as i64;
172 let tls = self.tls();
173 self.runtime.block_on(async {
174 if !stop.watched() {
177 return match tokio::time::timeout(self.timeout, exchange(request, tls)).await {
178 Ok(result) => result,
179 Err(_) => Err(Failure::TimedOut(millis)),
180 };
181 }
182 tokio::select! {
183 biased;
188 result = tokio::time::timeout(self.timeout, exchange(request, tls)) => match result {
189 Ok(result) => result,
190 Err(_) => Err(Failure::TimedOut(millis)),
191 },
192 () = watch(stop) => Err(Failure::Stopped),
193 }
194 })
195 }
196}
197
198async fn watch(stop: &Stop) {
204 loop {
205 if stop.asked() {
206 return;
207 }
208 tokio::time::sleep(POLL_MS).await;
209 }
210}
211
212async fn exchange(request: &Request, tls: Arc<ClientConfig>) -> Result<Reply, Failure> {
213 let authority = format!("{}:{}", request.host, request.port);
214 let stream = tokio::net::TcpStream::connect(&authority)
215 .await
216 .map_err(|e| Failure::Unreachable(e.to_string()))?;
217 if !request.tls {
218 return speak_http(stream, request, &authority).await;
219 }
220 let name = ServerName::try_from(request.host.to_string()).map_err(|_| {
224 Failure::Unreachable(format!(
225 "`{}` is not a name a certificate can be checked against",
226 request.host
227 ))
228 })?;
229 let stream = TlsConnector::from(tls)
230 .connect(name, stream)
231 .await
232 .map_err(|e| {
233 Failure::Unreachable(format!(
234 "the TLS handshake with `{}` failed: {e}",
235 request.host
236 ))
237 })?;
238 speak_http(stream, request, &authority).await
239}
240
241async fn speak_http<S>(stream: S, request: &Request, authority: &str) -> Result<Reply, Failure>
247where
248 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
249{
250 let (mut sender, conn) =
251 hyper::client::conn::http1::handshake(hyper_util::rt::TokioIo::new(stream))
252 .await
253 .map_err(|e| Failure::BadResponse(e.to_string()))?;
254 let pump = tokio::spawn(async move {
257 let _ = conn.await;
258 });
259
260 let mut builder = hyper::Request::builder()
261 .method(request.method.as_ref())
262 .uri(request.path.as_ref())
263 .header(hyper::header::HOST, authority);
266 for (name, value) in &request.headers {
267 builder = builder.header(name.as_ref(), value.as_ref());
268 }
269 let outgoing = builder
270 .body(Full::new(Bytes::from(request.body.as_bytes().to_vec())))
271 .map_err(|e| Failure::BadResponse(e.to_string()))?;
272
273 let response = sender
274 .send_request(outgoing)
275 .await
276 .map_err(|e| Failure::Unreachable(e.to_string()))?;
277 let status = response.status().as_u16() as i64;
278 let headers: Vec<(Arc<str>, Arc<str>)> = response
279 .headers()
280 .iter()
281 .filter_map(|(k, v)| {
282 v.to_str()
283 .ok()
284 .map(|v| (Arc::from(k.as_str()), Arc::from(v)))
285 })
286 .collect();
287 let body = Limited::new(response.into_body(), MAX_BODY_BYTES)
288 .collect()
289 .await
290 .map_err(|_| {
291 Failure::BadResponse(format!("the reply is longer than {MAX_BODY_BYTES} bytes"))
292 })?
293 .to_bytes();
294 pump.abort();
295 Ok(Reply {
296 status,
297 headers,
298 body: Arc::from(String::from_utf8_lossy(&body).as_ref()),
299 })
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use std::net::SocketAddr;
306
307 async fn echo_once(reply: &'static str) -> SocketAddr {
313 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
314 .await
315 .expect("a loopback port");
316 let addr = listener.local_addr().expect("an address");
317 tokio::spawn(async move {
318 let (stream, _) = listener.accept().await.expect("a connection");
319 let service = hyper::service::service_fn(
320 move |req: hyper::Request<hyper::body::Incoming>| async move {
321 let method = req.method().to_string();
322 let path = req.uri().path().to_string();
323 let hdr = req
324 .headers()
325 .get("x-beck")
326 .and_then(|v| v.to_str().ok())
327 .unwrap_or("")
328 .to_string();
329 let body = req.into_body().collect().await.expect("a body").to_bytes();
330 let seen = format!(
331 "{method} {path} x-beck={hdr} body={}",
332 String::from_utf8_lossy(&body)
333 );
334 Ok::<_, std::convert::Infallible>(
335 hyper::Response::builder()
336 .status(if reply == "seen" { 200 } else { 503 })
337 .header("x-reply", "yes")
338 .body(Full::new(Bytes::from(seen)))
339 .expect("a response"),
340 )
341 },
342 );
343 let _ = hyper::server::conn::http1::Builder::new()
344 .serve_connection(hyper_util::rt::TokioIo::new(stream), service)
345 .await;
346 });
347 addr
348 }
349
350 fn request(port: u16, path: &str) -> Request {
351 Request {
352 host: Arc::from("127.0.0.1"),
353 port,
354 tls: false,
355 method: Arc::from("POST"),
356 path: Arc::from(path),
357 headers: vec![(Arc::from("x-beck"), Arc::from("1"))],
358 body: Arc::from("hello"),
359 }
360 }
361
362 #[test]
363 fn a_request_reaches_a_real_server_and_the_reply_comes_back() {
364 let client = HttpOutbound::new().expect("a client");
365 let addr = client.runtime.block_on(echo_once("seen"));
366 let reply = client
367 .fetch(&request(addr.port(), "/v1/things?x=1"), &Stop::never())
368 .expect("a reply");
369 assert_eq!(reply.status, 200);
370 assert_eq!(
371 reply.body.as_ref(),
372 "POST /v1/things x-beck=1 body=hello",
373 "the server saw the method, the path, the header and the body"
374 );
375 assert!(
376 reply
377 .headers
378 .iter()
379 .any(|(k, v)| k.as_ref() == "x-reply" && v.as_ref() == "yes"),
380 "{:?}",
381 reply.headers
382 );
383 }
384
385 #[test]
386 fn a_status_is_a_reply_and_not_a_failure() {
387 let client = HttpOutbound::new().expect("a client");
388 let addr = client.runtime.block_on(echo_once("no"));
389 let reply = client
390 .fetch(&request(addr.port(), "/gone"), &Stop::never())
391 .expect("a reply");
392 assert_eq!(reply.status, 503);
393 assert!(reply.body.contains("/gone"), "the body survives a 503");
394 }
395
396 #[test]
403 fn a_watched_request_is_given_up_when_the_caller_stops_wanting_it() {
404 let client = HttpOutbound::new().expect("a client");
405 let addr = client.runtime.block_on(async {
406 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
407 .await
408 .expect("a port");
409 let addr = listener.local_addr().expect("an address");
410 tokio::spawn(async move {
411 let held = listener.accept().await;
413 std::future::pending::<()>().await;
414 drop(held);
415 });
416 addr
417 });
418
419 let asked = Arc::new(std::sync::atomic::AtomicBool::new(false));
420 let stop = {
421 let asked = Arc::clone(&asked);
422 Stop::when(move || asked.load(std::sync::atomic::Ordering::SeqCst))
423 };
424 let flip = {
427 let asked = Arc::clone(&asked);
428 std::thread::spawn(move || {
429 std::thread::sleep(Duration::from_millis(50));
430 asked.store(true, std::sync::atomic::Ordering::SeqCst);
431 })
432 };
433
434 let out = client.fetch(&request(addr.port(), "/hangs"), &stop);
435 flip.join().expect("the flipping thread");
436 assert!(
437 matches!(out, Err(Failure::Stopped)),
438 "the client should give up on a stopped request rather than wait out its timeout, and \
439 it answered {out:?}"
440 );
441 }
442
443 #[test]
444 fn nothing_listening_is_unreachable_rather_than_a_panic() {
445 let client = HttpOutbound::new().expect("a client");
446 match client.fetch(&request(1, "/"), &Stop::never()) {
448 Err(Failure::Unreachable(_)) => {}
449 other => panic!("expected unreachable, got {other:?}"),
450 }
451 }
452
453 fn certificate_authority() -> (
462 rcgen::Issuer<'static, rcgen::KeyPair>,
463 tokio_rustls::rustls::RootCertStore,
464 ) {
465 let mut params = rcgen::CertificateParams::new(Vec::new()).expect("no names on a CA");
466 params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
467 let key = rcgen::KeyPair::generate().expect("a key pair");
468 let ca = params.self_signed(&key).expect("a self-signed CA");
469 let mut roots = tokio_rustls::rustls::RootCertStore::empty();
470 roots
471 .add(ca.der().clone())
472 .expect("the CA is a certificate");
473 (rcgen::Issuer::new(params, key), roots)
474 }
475
476 async fn tls_echo_once(
479 issuer: &rcgen::Issuer<'static, rcgen::KeyPair>,
480 claims: &str,
481 ) -> SocketAddr {
482 tls_echo_once_speaking(issuer, claims, &[&tokio_rustls::rustls::version::TLS13]).await
483 }
484
485 async fn tls_echo_once_speaking(
491 issuer: &rcgen::Issuer<'static, rcgen::KeyPair>,
492 claims: &str,
493 versions: &[&'static tokio_rustls::rustls::SupportedProtocolVersion],
494 ) -> SocketAddr {
495 let params =
496 rcgen::CertificateParams::new(vec![claims.to_string()]).expect("a subject alt name");
497 let key = rcgen::KeyPair::generate().expect("a key pair");
498 let leaf = params.signed_by(&key, issuer).expect("issued by the CA");
499
500 let config = tokio_rustls::rustls::ServerConfig::builder_with_provider(Arc::new(
501 tokio_rustls::rustls::crypto::aws_lc_rs::default_provider(),
502 ))
503 .with_protocol_versions(versions)
504 .expect("the protocol versions are supported by the provider")
505 .with_no_client_auth()
506 .with_single_cert(
507 vec![leaf.der().clone()],
508 tokio_rustls::rustls::pki_types::PrivateKeyDer::Pkcs8(key.serialize_der().into()),
509 )
510 .expect("a server configuration");
511
512 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
513 .await
514 .expect("a loopback port");
515 let addr = listener.local_addr().expect("an address");
516 tokio::spawn(async move {
517 let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
518 let (stream, _) = listener.accept().await.expect("a connection");
519 let Ok(stream) = acceptor.accept(stream).await else {
520 return;
521 };
522 let service = hyper::service::service_fn(
523 |_req: hyper::Request<hyper::body::Incoming>| async move {
524 Ok::<_, std::convert::Infallible>(hyper::Response::new(Full::new(
525 Bytes::from_static(b"ok, privately"),
526 )))
527 },
528 );
529 let _ = hyper::server::conn::http1::Builder::new()
530 .serve_connection(hyper_util::rt::TokioIo::new(stream), service)
531 .await;
532 });
533 addr
534 }
535
536 fn secure_request(host: &str, port: u16) -> Request {
537 Request {
538 host: Arc::from(host),
539 port,
540 tls: true,
541 method: Arc::from("GET"),
542 path: Arc::from("/"),
543 headers: Vec::new(),
544 body: Arc::from(""),
545 }
546 }
547
548 #[test]
561 fn a_peer_that_speaks_only_tls_1_2_is_refused() {
562 let (issuer, roots) = certificate_authority();
563 let client = HttpOutbound::trusting(roots).expect("a client");
564
565 let legacy = client.runtime.block_on(tls_echo_once_speaking(
566 &issuer,
567 "127.0.0.1",
568 &[&tokio_rustls::rustls::version::TLS12],
569 ));
570 let refused = client.fetch(&secure_request("127.0.0.1", legacy.port()), &Stop::never());
571 assert!(
572 refused.is_err(),
573 "a 1.2-only peer was accepted: {refused:?}"
574 );
575
576 let modern = client.runtime.block_on(tls_echo_once(&issuer, "127.0.0.1"));
577 let reply = client
578 .fetch(&secure_request("127.0.0.1", modern.port()), &Stop::never())
579 .expect("the same certificate over 1.3 is fine");
580 assert_eq!(reply.body.as_ref(), "ok, privately");
581 }
582
583 #[test]
591 fn tls_verifies_the_name_the_call_site_wrote() {
592 let (issuer, roots) = certificate_authority();
593 let client = HttpOutbound::trusting(roots).expect("a client");
594
595 let addr = client.runtime.block_on(tls_echo_once(&issuer, "127.0.0.1"));
596 let reply = client
597 .fetch(&secure_request("127.0.0.1", addr.port()), &Stop::never())
598 .expect("the handshake completes and the reply comes back");
599 assert_eq!(reply.status, 200);
600 assert_eq!(reply.body.as_ref(), "ok, privately");
601
602 let (issuer, roots) = certificate_authority();
604 let client = HttpOutbound::trusting(roots).expect("a client");
605 let addr = client
606 .runtime
607 .block_on(tls_echo_once(&issuer, "elsewhere.test"));
608 match client.fetch(&secure_request("127.0.0.1", addr.port()), &Stop::never()) {
609 Err(Failure::Unreachable(why)) => assert!(why.contains("handshake"), "{why}"),
610 other => panic!("a certificate for another name was accepted: {other:?}"),
611 }
612 }
613
614 #[test]
617 fn a_plaintext_peer_does_not_answer_a_request_that_asked_for_tls() {
618 let client = HttpOutbound::new().expect("a client");
619 let addr = client.runtime.block_on(echo_once("seen"));
620 match client.fetch(&secure_request("127.0.0.1", addr.port()), &Stop::never()) {
621 Err(Failure::Unreachable(why)) => assert!(why.contains("handshake"), "{why}"),
622 other => panic!("a plaintext peer answered a TLS request: {other:?}"),
623 }
624 }
625}