1pub mod idle;
10pub mod tls;
11
12use std::collections::HashMap;
13use std::sync::{Arc, OnceLock};
14use std::time::Duration;
15
16use bytes::Bytes;
17use http_body_util::combinators::BoxBody;
18use http_body_util::{BodyExt, Full};
19use hyper_rustls::HttpsConnector;
20use hyper_util::client::legacy::connect::HttpConnector;
21use hyper_util::client::legacy::Client;
22use hyper_util::rt::TokioExecutor;
23
24type PooledClient = Client<HttpsConnector<HttpConnector>, Full<Bytes>>;
25
26pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
42
43pub struct OutboundRequest {
50 pub method: http::Method,
51 pub url: String,
52 pub headers: Vec<(String, String)>,
54 pub body: Bytes,
55 pub timeout: Duration,
59 pub ssl_verify: bool,
63 pub tls: Option<Arc<tls::UpstreamTls>>,
66}
67
68impl OutboundRequest {
69 #[allow(dead_code)] pub fn new(method: http::Method, url: impl Into<String>) -> Self {
72 Self {
73 method,
74 url: url.into(),
75 headers: Vec::new(),
76 body: Bytes::new(),
77 timeout: Duration::from_secs(3),
78 ssl_verify: true,
79 tls: None,
80 }
81 }
82}
83
84pub struct OutboundResponse {
86 pub status: u16,
87 pub headers: HashMap<String, Vec<String>>,
88 pub body: Bytes,
89}
90
91pub struct OutboundStreamingResponse {
93 pub status: u16,
94 pub headers: HashMap<String, Vec<String>>,
95 pub body: BoxBody<Bytes, BoxError>,
96}
97
98#[derive(Debug)]
101pub enum OutboundError {
102 Timeout(Duration),
103 InvalidRequest(String),
105 Transport(String),
107}
108
109impl std::fmt::Display for OutboundError {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Self::Timeout(d) => write!(f, "outbound request timed out after {:?}", d),
113 Self::InvalidRequest(m) => write!(f, "invalid outbound request: {}", m),
114 Self::Transport(m) => write!(f, "outbound transport error: {}", m),
115 }
116 }
117}
118
119pub struct OutboundClient {
121 verified: PooledClient,
122 insecure: OnceLock<PooledClient>,
125 custom: std::sync::RwLock<HashMap<u64, PooledClient>>,
129}
130
131impl OutboundClient {
132 pub fn new() -> Self {
137 crate::server::tls::install_crypto_provider();
140
141 let https = hyper_rustls::HttpsConnectorBuilder::new()
142 .with_native_roots()
143 .expect("failed to load native TLS roots")
144 .https_or_http()
145 .enable_http1()
146 .enable_http2()
147 .build();
148 Self {
149 verified: Client::builder(TokioExecutor::new()).build(https),
150 insecure: OnceLock::new(),
151 custom: std::sync::RwLock::new(HashMap::new()),
152 }
153 }
154
155 pub async fn request(&self, req: OutboundRequest) -> Result<OutboundResponse, OutboundError> {
157 let deadline = req.timeout;
158 let call = async {
159 let response = self.dispatch(&req).await?;
160
161 let status = response.status().as_u16();
162 let mut headers: HashMap<String, Vec<String>> = HashMap::new();
163 for (name, value) in response.headers() {
164 headers
165 .entry(name.as_str().to_string())
166 .or_default()
167 .push(value.to_str().unwrap_or("").to_string());
168 }
169 let body = response
170 .into_body()
171 .collect()
172 .await
173 .map_err(|e| OutboundError::Transport(e.to_string()))?
174 .to_bytes();
175
176 Ok(OutboundResponse {
177 status,
178 headers,
179 body,
180 })
181 };
182
183 tokio::time::timeout(deadline, call)
184 .await
185 .map_err(|_| OutboundError::Timeout(deadline))?
186 }
187
188 pub async fn request_streaming(
192 &self,
193 req: OutboundRequest,
194 ) -> Result<OutboundStreamingResponse, OutboundError> {
195 let deadline = req.timeout;
196 let call = async {
197 let response = self.dispatch(&req).await?;
198
199 let status = response.status().as_u16();
200 let mut headers: HashMap<String, Vec<String>> = HashMap::new();
201 for (name, value) in response.headers() {
202 headers
203 .entry(name.as_str().to_string())
204 .or_default()
205 .push(value.to_str().unwrap_or("").to_string());
206 }
207 let body: BoxBody<Bytes, BoxError> = response.into_body().map_err(Into::into).boxed();
213
214 Ok(OutboundStreamingResponse {
215 status,
216 headers,
217 body,
218 })
219 };
220
221 tokio::time::timeout(deadline, call)
222 .await
223 .map_err(|_| OutboundError::Timeout(deadline))?
224 }
225
226 async fn dispatch(
230 &self,
231 req: &OutboundRequest,
232 ) -> Result<http::Response<hyper::body::Incoming>, OutboundError> {
233 let mut builder = http::Request::builder()
234 .method(req.method.clone())
235 .uri(&req.url);
236 for (name, value) in &req.headers {
237 builder = builder.header(name.as_str(), value.as_str());
238 }
239 let request = builder
240 .body(Full::new(req.body.clone()))
241 .map_err(|e| OutboundError::InvalidRequest(e.to_string()))?;
242
243 let custom_client;
244 let client = match &req.tls {
245 Some(identity) => {
246 custom_client = self.identity_client(identity)?;
247 &custom_client
248 }
249 None if req.ssl_verify => &self.verified,
250 None => self.insecure.get_or_init(build_insecure_client),
251 };
252
253 client
254 .request(request)
255 .await
256 .map_err(|e| OutboundError::Transport(e.to_string()))
257 }
258
259 fn identity_client(
263 &self,
264 identity: &Arc<tls::UpstreamTls>,
265 ) -> Result<PooledClient, OutboundError> {
266 if let Some(c) = self.custom.read().unwrap().get(&identity.cache_key()) {
267 return Ok(c.clone());
268 }
269 let config = identity
272 .client_config()
273 .map_err(OutboundError::InvalidRequest)?;
274 let https = hyper_rustls::HttpsConnectorBuilder::new()
275 .with_tls_config(config)
276 .https_or_http()
277 .enable_http1()
278 .enable_http2()
279 .build();
280 let client = Client::builder(TokioExecutor::new()).build(https);
281 Ok(self
283 .custom
284 .write()
285 .unwrap()
286 .entry(identity.cache_key())
287 .or_insert(client)
288 .clone())
289 }
290}
291
292impl Default for OutboundClient {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298fn build_insecure_client() -> PooledClient {
299 let config = rustls::ClientConfig::builder()
300 .dangerous()
301 .with_custom_certificate_verifier(Arc::new(NoVerification(
302 rustls::crypto::ring::default_provider(),
303 )))
304 .with_no_client_auth();
305 let https = hyper_rustls::HttpsConnectorBuilder::new()
306 .with_tls_config(config)
307 .https_or_http()
308 .enable_http1()
309 .build();
310 Client::builder(TokioExecutor::new()).build(https)
311}
312
313pub fn client_tls_connector(
324 verify: bool,
325 identity: Option<&Arc<tls::UpstreamTls>>,
326) -> Result<tokio_rustls::TlsConnector, String> {
327 crate::server::tls::install_crypto_provider();
328
329 if let Some(id) = identity {
330 static CUSTOM: OnceLock<std::sync::RwLock<HashMap<u64, tokio_rustls::TlsConnector>>> =
331 OnceLock::new();
332 let cache = CUSTOM.get_or_init(|| std::sync::RwLock::new(HashMap::new()));
333 if let Some(c) = cache.read().unwrap().get(&id.cache_key()) {
334 return Ok(c.clone());
335 }
336 let mut config = id.client_config()?;
337 config.alpn_protocols = vec![b"http/1.1".to_vec()];
338 let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
339 return Ok(cache
340 .write()
341 .unwrap()
342 .entry(id.cache_key())
343 .or_insert(connector)
344 .clone());
345 }
346
347 if !verify {
348 let config = rustls::ClientConfig::builder()
349 .dangerous()
350 .with_custom_certificate_verifier(Arc::new(NoVerification(
351 rustls::crypto::ring::default_provider(),
352 )))
353 .with_no_client_auth();
354 let mut config = config;
355 config.alpn_protocols = vec![b"http/1.1".to_vec()];
356 return Ok(tokio_rustls::TlsConnector::from(Arc::new(config)));
357 }
358
359 static VERIFIED: OnceLock<tokio_rustls::TlsConnector> = OnceLock::new();
360 if let Some(c) = VERIFIED.get() {
361 return Ok(c.clone());
362 }
363
364 let mut roots = rustls::RootCertStore::empty();
365 let loaded = rustls_native_certs::load_native_certs();
366 let (added, _ignored) = roots.add_parsable_certificates(loaded.certs);
367 if added == 0 {
368 return Err(format!(
369 "no usable native root certificates ({} load error(s))",
370 loaded.errors.len()
371 ));
372 }
373 let mut config = rustls::ClientConfig::builder()
374 .with_root_certificates(roots)
375 .with_no_client_auth();
376 config.alpn_protocols = vec![b"http/1.1".to_vec()];
377 let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
378 Ok(VERIFIED.get_or_init(|| connector).clone())
380}
381
382#[derive(Debug)]
385pub(crate) struct NoVerification(pub(crate) rustls::crypto::CryptoProvider);
386
387impl rustls::client::danger::ServerCertVerifier for NoVerification {
388 fn verify_server_cert(
389 &self,
390 _end_entity: &rustls::pki_types::CertificateDer<'_>,
391 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
392 _server_name: &rustls::pki_types::ServerName<'_>,
393 _ocsp_response: &[u8],
394 _now: rustls::pki_types::UnixTime,
395 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
396 Ok(rustls::client::danger::ServerCertVerified::assertion())
397 }
398
399 fn verify_tls12_signature(
400 &self,
401 message: &[u8],
402 cert: &rustls::pki_types::CertificateDer<'_>,
403 dss: &rustls::DigitallySignedStruct,
404 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
405 rustls::crypto::verify_tls12_signature(
406 message,
407 cert,
408 dss,
409 &self.0.signature_verification_algorithms,
410 )
411 }
412
413 fn verify_tls13_signature(
414 &self,
415 message: &[u8],
416 cert: &rustls::pki_types::CertificateDer<'_>,
417 dss: &rustls::DigitallySignedStruct,
418 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
419 rustls::crypto::verify_tls13_signature(
420 message,
421 cert,
422 dss,
423 &self.0.signature_verification_algorithms,
424 )
425 }
426
427 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
428 self.0.signature_verification_algorithms.supported_schemes()
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[tokio::test]
441 async fn test_request_streaming_returns_after_headers() {
442 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
443 let port = listener.local_addr().unwrap().port();
444 tokio::spawn(async move {
445 if let Ok((mut stream, _)) = listener.accept().await {
446 use tokio::io::{AsyncReadExt, AsyncWriteExt};
447 let mut buf = [0u8; 4096];
448 let _ = stream.read(&mut buf).await;
449 let _ = stream
451 .write_all(
452 b"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\n\
453 transfer-encoding: chunked\r\n\r\n",
454 )
455 .await;
456 tokio::time::sleep(std::time::Duration::from_secs(30)).await;
457 }
458 });
459
460 let client = OutboundClient::new();
461 let req = OutboundRequest {
462 method: http::Method::GET,
463 url: format!("http://127.0.0.1:{port}/stream"),
464 headers: Vec::new(),
465 body: Bytes::new(),
466 timeout: std::time::Duration::from_secs(5),
467 ssl_verify: true,
468 tls: None,
469 };
470
471 let resp = tokio::time::timeout(
472 std::time::Duration::from_secs(3),
473 client.request_streaming(req),
474 )
475 .await
476 .expect("request_streaming must return before the body arrives")
477 .expect("streaming call succeeded");
478
479 assert_eq!(resp.status, 200);
480 assert_eq!(
481 resp.headers.get("content-type").map(|v| v[0].as_str()),
482 Some("text/event-stream")
483 );
484 }
485
486 #[test]
487 fn test_client_tls_connector_insecure_builds() {
488 assert!(client_tls_connector(false, None).is_ok());
490 }
491
492 #[test]
493 fn test_client_tls_connector_verified_builds() {
494 assert!(client_tls_connector(true, None).is_ok());
497 }
498
499 #[test]
500 fn test_client_tls_connector_with_identity_builds_and_caches() {
501 let (cert, key, ca) = {
504 let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
505 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
506 let ca_key = rcgen::KeyPair::generate().unwrap();
507 let ca_cert = ca_params.self_signed(&ca_key).unwrap();
508 let ca_issuer = rcgen::Issuer::from_ca_cert_der(ca_cert.der(), &ca_key).unwrap();
509 let leaf_params = rcgen::CertificateParams::new(vec!["client".to_string()]).unwrap();
510 let leaf_key = rcgen::KeyPair::generate().unwrap();
511 let leaf_cert = leaf_params.signed_by(&leaf_key, &ca_issuer).unwrap();
512 let dir = std::env::temp_dir();
513 let pid = std::process::id();
514 let cert = dir.join(format!("featherbit_wsid_{}.crt", pid));
515 let key = dir.join(format!("featherbit_wsid_{}.key", pid));
516 let ca = dir.join(format!("featherbit_wsid_{}.ca.crt", pid));
517 std::fs::write(&cert, leaf_cert.pem()).unwrap();
518 std::fs::write(&key, leaf_key.serialize_pem()).unwrap();
519 std::fs::write(&ca, ca_cert.pem()).unwrap();
520 (
521 cert.to_str().unwrap().to_string(),
522 key.to_str().unwrap().to_string(),
523 ca.to_str().unwrap().to_string(),
524 )
525 };
526 let identity = tls::UpstreamTls::load(Some((&cert, &key)), Some(&ca), true).unwrap();
527 assert!(client_tls_connector(true, Some(&identity)).is_ok());
528 assert!(client_tls_connector(true, Some(&identity)).is_ok());
530 assert!(client_tls_connector(true, None).is_ok());
532 assert!(client_tls_connector(false, None).is_ok());
533 }
534
535 async fn spawn_mtls_server(server_config: Arc<rustls::ServerConfig>) -> u16 {
538 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
539 let port = listener.local_addr().unwrap().port();
540 tokio::spawn(async move {
541 let acceptor = tokio_rustls::TlsAcceptor::from(server_config);
542 loop {
545 let Ok((tcp, _)) = listener.accept().await else {
546 break;
547 };
548 let acceptor = acceptor.clone();
549 tokio::spawn(async move {
550 if let Ok(mut stream) = acceptor.accept(tcp).await {
551 use tokio::io::{AsyncReadExt, AsyncWriteExt};
552 let mut buf = [0u8; 4096];
553 let _ = stream.read(&mut buf).await;
554 let _ = stream
555 .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")
556 .await;
557 let _ = stream.shutdown().await;
558 }
559 });
560 }
561 });
562 port
563 }
564
565 #[tokio::test]
566 async fn test_request_with_client_identity_reaches_mtls_backend() {
567 crate::server::tls::install_crypto_provider();
568
569 let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
571 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
572 let ca_key = rcgen::KeyPair::generate().unwrap();
573 let ca_cert = ca_params.self_signed(&ca_key).unwrap();
574 let ca_issuer = rcgen::Issuer::from_ca_cert_der(ca_cert.der(), &ca_key).unwrap();
575
576 let server_params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
577 let server_key = rcgen::KeyPair::generate().unwrap();
578 let server_cert = server_params.signed_by(&server_key, &ca_issuer).unwrap();
579
580 let client_params = rcgen::CertificateParams::new(vec!["gw".to_string()]).unwrap();
581 let client_key = rcgen::KeyPair::generate().unwrap();
582 let client_cert = client_params.signed_by(&client_key, &ca_issuer).unwrap();
583
584 let mut roots = rustls::RootCertStore::empty();
586 roots
587 .add(rustls::pki_types::CertificateDer::from(
588 ca_cert.der().to_vec(),
589 ))
590 .unwrap();
591 let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
592 .build()
593 .unwrap();
594 let server_config = rustls::ServerConfig::builder()
595 .with_client_cert_verifier(verifier)
596 .with_single_cert(
597 vec![rustls::pki_types::CertificateDer::from(
598 server_cert.der().to_vec(),
599 )],
600 rustls::pki_types::PrivateKeyDer::try_from(server_key.serialize_der()).unwrap(),
601 )
602 .unwrap();
603 let port = spawn_mtls_server(Arc::new(server_config)).await;
604
605 let dir = std::env::temp_dir();
607 let pid = std::process::id();
608 let cert_path = dir.join(format!("featherbit_ob_mtls_{}.crt", pid));
609 let key_path = dir.join(format!("featherbit_ob_mtls_{}.key", pid));
610 let ca_path = dir.join(format!("featherbit_ob_mtls_{}.ca.crt", pid));
611 std::fs::write(&cert_path, client_cert.pem()).unwrap();
612 std::fs::write(&key_path, client_key.serialize_pem()).unwrap();
613 std::fs::write(&ca_path, ca_cert.pem()).unwrap();
614
615 let identity = tls::UpstreamTls::load(
616 Some((cert_path.to_str().unwrap(), key_path.to_str().unwrap())),
617 Some(ca_path.to_str().unwrap()),
618 true,
619 )
620 .unwrap();
621
622 let client = OutboundClient::new();
623 let ok = client
624 .request(OutboundRequest {
625 method: http::Method::GET,
626 url: format!("https://localhost:{}/", port),
627 headers: Vec::new(),
628 body: Bytes::new(),
629 timeout: Duration::from_secs(5),
630 ssl_verify: true,
631 tls: Some(identity),
632 })
633 .await
634 .expect("mTLS request should succeed");
635 assert_eq!(ok.status, 200);
636
637 let ca_only = tls::UpstreamTls::load(None, Some(ca_path.to_str().unwrap()), true).unwrap();
639 let err = client
640 .request(OutboundRequest {
641 method: http::Method::GET,
642 url: format!("https://localhost:{}/", port),
643 headers: Vec::new(),
644 body: Bytes::new(),
645 timeout: Duration::from_secs(5),
646 ssl_verify: true,
647 tls: Some(ca_only),
648 })
649 .await;
650 assert!(
651 matches!(err, Err(OutboundError::Transport(_))),
652 "got: {:?}",
653 err.map(|r| r.status)
654 );
655 }
656}