1pub mod tls;
10
11use std::collections::HashMap;
12use std::sync::{Arc, OnceLock};
13use std::time::Duration;
14
15use bytes::Bytes;
16use http_body_util::{BodyExt, Full};
17use hyper_rustls::HttpsConnector;
18use hyper_util::client::legacy::connect::HttpConnector;
19use hyper_util::client::legacy::Client;
20use hyper_util::rt::TokioExecutor;
21
22type PooledClient = Client<HttpsConnector<HttpConnector>, Full<Bytes>>;
23
24pub struct OutboundRequest {
27 pub method: http::Method,
28 pub url: String,
29 pub headers: Vec<(String, String)>,
31 pub body: Bytes,
32 pub timeout: Duration,
34 pub ssl_verify: bool,
38 pub tls: Option<Arc<tls::UpstreamTls>>,
41}
42
43impl OutboundRequest {
44 #[allow(dead_code)] pub fn new(method: http::Method, url: impl Into<String>) -> Self {
47 Self {
48 method,
49 url: url.into(),
50 headers: Vec::new(),
51 body: Bytes::new(),
52 timeout: Duration::from_secs(3),
53 ssl_verify: true,
54 tls: None,
55 }
56 }
57}
58
59pub struct OutboundResponse {
61 pub status: u16,
62 pub headers: HashMap<String, Vec<String>>,
63 pub body: Bytes,
64}
65
66#[derive(Debug)]
69pub enum OutboundError {
70 Timeout(Duration),
71 InvalidRequest(String),
73 Transport(String),
75}
76
77impl std::fmt::Display for OutboundError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::Timeout(d) => write!(f, "outbound request timed out after {:?}", d),
81 Self::InvalidRequest(m) => write!(f, "invalid outbound request: {}", m),
82 Self::Transport(m) => write!(f, "outbound transport error: {}", m),
83 }
84 }
85}
86
87pub struct OutboundClient {
89 verified: PooledClient,
90 insecure: OnceLock<PooledClient>,
93 custom: std::sync::RwLock<HashMap<u64, PooledClient>>,
97}
98
99impl OutboundClient {
100 pub fn new() -> Self {
105 crate::server::tls::install_crypto_provider();
108
109 let https = hyper_rustls::HttpsConnectorBuilder::new()
110 .with_native_roots()
111 .expect("failed to load native TLS roots")
112 .https_or_http()
113 .enable_http1()
114 .enable_http2()
115 .build();
116 Self {
117 verified: Client::builder(TokioExecutor::new()).build(https),
118 insecure: OnceLock::new(),
119 custom: std::sync::RwLock::new(HashMap::new()),
120 }
121 }
122
123 pub async fn request(&self, req: OutboundRequest) -> Result<OutboundResponse, OutboundError> {
125 let mut builder = http::Request::builder().method(req.method).uri(&req.url);
126 for (name, value) in &req.headers {
127 builder = builder.header(name.as_str(), value.as_str());
128 }
129 let request = builder
130 .body(Full::new(req.body))
131 .map_err(|e| OutboundError::InvalidRequest(e.to_string()))?;
132
133 let custom_client;
134 let client = match &req.tls {
135 Some(identity) => {
136 custom_client = self.identity_client(identity)?;
137 &custom_client
138 }
139 None if req.ssl_verify => &self.verified,
140 None => self.insecure.get_or_init(build_insecure_client),
141 };
142
143 let deadline = req.timeout;
144 let call = async {
145 let response = client
146 .request(request)
147 .await
148 .map_err(|e| OutboundError::Transport(e.to_string()))?;
149
150 let status = response.status().as_u16();
151 let mut headers: HashMap<String, Vec<String>> = HashMap::new();
152 for (name, value) in response.headers() {
153 headers
154 .entry(name.as_str().to_string())
155 .or_default()
156 .push(value.to_str().unwrap_or("").to_string());
157 }
158 let body = response
159 .into_body()
160 .collect()
161 .await
162 .map_err(|e| OutboundError::Transport(e.to_string()))?
163 .to_bytes();
164
165 Ok(OutboundResponse {
166 status,
167 headers,
168 body,
169 })
170 };
171
172 tokio::time::timeout(deadline, call)
173 .await
174 .map_err(|_| OutboundError::Timeout(deadline))?
175 }
176
177 fn identity_client(
181 &self,
182 identity: &Arc<tls::UpstreamTls>,
183 ) -> Result<PooledClient, OutboundError> {
184 if let Some(c) = self.custom.read().unwrap().get(&identity.cache_key()) {
185 return Ok(c.clone());
186 }
187 let config = identity
190 .client_config()
191 .map_err(OutboundError::InvalidRequest)?;
192 let https = hyper_rustls::HttpsConnectorBuilder::new()
193 .with_tls_config(config)
194 .https_or_http()
195 .enable_http1()
196 .enable_http2()
197 .build();
198 let client = Client::builder(TokioExecutor::new()).build(https);
199 Ok(self
201 .custom
202 .write()
203 .unwrap()
204 .entry(identity.cache_key())
205 .or_insert(client)
206 .clone())
207 }
208}
209
210impl Default for OutboundClient {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216fn build_insecure_client() -> PooledClient {
217 let config = rustls::ClientConfig::builder()
218 .dangerous()
219 .with_custom_certificate_verifier(Arc::new(NoVerification(
220 rustls::crypto::ring::default_provider(),
221 )))
222 .with_no_client_auth();
223 let https = hyper_rustls::HttpsConnectorBuilder::new()
224 .with_tls_config(config)
225 .https_or_http()
226 .enable_http1()
227 .build();
228 Client::builder(TokioExecutor::new()).build(https)
229}
230
231pub fn client_tls_connector(
242 verify: bool,
243 identity: Option<&Arc<tls::UpstreamTls>>,
244) -> Result<tokio_rustls::TlsConnector, String> {
245 crate::server::tls::install_crypto_provider();
246
247 if let Some(id) = identity {
248 static CUSTOM: OnceLock<std::sync::RwLock<HashMap<u64, tokio_rustls::TlsConnector>>> =
249 OnceLock::new();
250 let cache = CUSTOM.get_or_init(|| std::sync::RwLock::new(HashMap::new()));
251 if let Some(c) = cache.read().unwrap().get(&id.cache_key()) {
252 return Ok(c.clone());
253 }
254 let mut config = id.client_config()?;
255 config.alpn_protocols = vec![b"http/1.1".to_vec()];
256 let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
257 return Ok(cache
258 .write()
259 .unwrap()
260 .entry(id.cache_key())
261 .or_insert(connector)
262 .clone());
263 }
264
265 if !verify {
266 let config = rustls::ClientConfig::builder()
267 .dangerous()
268 .with_custom_certificate_verifier(Arc::new(NoVerification(
269 rustls::crypto::ring::default_provider(),
270 )))
271 .with_no_client_auth();
272 let mut config = config;
273 config.alpn_protocols = vec![b"http/1.1".to_vec()];
274 return Ok(tokio_rustls::TlsConnector::from(Arc::new(config)));
275 }
276
277 static VERIFIED: OnceLock<tokio_rustls::TlsConnector> = OnceLock::new();
278 if let Some(c) = VERIFIED.get() {
279 return Ok(c.clone());
280 }
281
282 let mut roots = rustls::RootCertStore::empty();
283 let loaded = rustls_native_certs::load_native_certs();
284 let (added, _ignored) = roots.add_parsable_certificates(loaded.certs);
285 if added == 0 {
286 return Err(format!(
287 "no usable native root certificates ({} load error(s))",
288 loaded.errors.len()
289 ));
290 }
291 let mut config = rustls::ClientConfig::builder()
292 .with_root_certificates(roots)
293 .with_no_client_auth();
294 config.alpn_protocols = vec![b"http/1.1".to_vec()];
295 let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
296 Ok(VERIFIED.get_or_init(|| connector).clone())
298}
299
300#[derive(Debug)]
303pub(crate) struct NoVerification(pub(crate) rustls::crypto::CryptoProvider);
304
305impl rustls::client::danger::ServerCertVerifier for NoVerification {
306 fn verify_server_cert(
307 &self,
308 _end_entity: &rustls::pki_types::CertificateDer<'_>,
309 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
310 _server_name: &rustls::pki_types::ServerName<'_>,
311 _ocsp_response: &[u8],
312 _now: rustls::pki_types::UnixTime,
313 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
314 Ok(rustls::client::danger::ServerCertVerified::assertion())
315 }
316
317 fn verify_tls12_signature(
318 &self,
319 message: &[u8],
320 cert: &rustls::pki_types::CertificateDer<'_>,
321 dss: &rustls::DigitallySignedStruct,
322 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
323 rustls::crypto::verify_tls12_signature(
324 message,
325 cert,
326 dss,
327 &self.0.signature_verification_algorithms,
328 )
329 }
330
331 fn verify_tls13_signature(
332 &self,
333 message: &[u8],
334 cert: &rustls::pki_types::CertificateDer<'_>,
335 dss: &rustls::DigitallySignedStruct,
336 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
337 rustls::crypto::verify_tls13_signature(
338 message,
339 cert,
340 dss,
341 &self.0.signature_verification_algorithms,
342 )
343 }
344
345 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
346 self.0.signature_verification_algorithms.supported_schemes()
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_client_tls_connector_insecure_builds() {
356 assert!(client_tls_connector(false, None).is_ok());
358 }
359
360 #[test]
361 fn test_client_tls_connector_verified_builds() {
362 assert!(client_tls_connector(true, None).is_ok());
365 }
366
367 #[test]
368 fn test_client_tls_connector_with_identity_builds_and_caches() {
369 let (cert, key, ca) = {
372 let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
373 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
374 let ca_key = rcgen::KeyPair::generate().unwrap();
375 let ca_cert = ca_params.self_signed(&ca_key).unwrap();
376 let leaf_params = rcgen::CertificateParams::new(vec!["client".to_string()]).unwrap();
377 let leaf_key = rcgen::KeyPair::generate().unwrap();
378 let leaf_cert = leaf_params.signed_by(&leaf_key, &ca_cert, &ca_key).unwrap();
379 let dir = std::env::temp_dir();
380 let pid = std::process::id();
381 let cert = dir.join(format!("featherbit_wsid_{}.crt", pid));
382 let key = dir.join(format!("featherbit_wsid_{}.key", pid));
383 let ca = dir.join(format!("featherbit_wsid_{}.ca.crt", pid));
384 std::fs::write(&cert, leaf_cert.pem()).unwrap();
385 std::fs::write(&key, leaf_key.serialize_pem()).unwrap();
386 std::fs::write(&ca, ca_cert.pem()).unwrap();
387 (
388 cert.to_str().unwrap().to_string(),
389 key.to_str().unwrap().to_string(),
390 ca.to_str().unwrap().to_string(),
391 )
392 };
393 let identity = tls::UpstreamTls::load(Some((&cert, &key)), Some(&ca), true).unwrap();
394 assert!(client_tls_connector(true, Some(&identity)).is_ok());
395 assert!(client_tls_connector(true, Some(&identity)).is_ok());
397 assert!(client_tls_connector(true, None).is_ok());
399 assert!(client_tls_connector(false, None).is_ok());
400 }
401
402 async fn spawn_mtls_server(server_config: Arc<rustls::ServerConfig>) -> u16 {
405 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
406 let port = listener.local_addr().unwrap().port();
407 tokio::spawn(async move {
408 let acceptor = tokio_rustls::TlsAcceptor::from(server_config);
409 loop {
412 let Ok((tcp, _)) = listener.accept().await else {
413 break;
414 };
415 let acceptor = acceptor.clone();
416 tokio::spawn(async move {
417 if let Ok(mut stream) = acceptor.accept(tcp).await {
418 use tokio::io::{AsyncReadExt, AsyncWriteExt};
419 let mut buf = [0u8; 4096];
420 let _ = stream.read(&mut buf).await;
421 let _ = stream
422 .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")
423 .await;
424 let _ = stream.shutdown().await;
425 }
426 });
427 }
428 });
429 port
430 }
431
432 #[tokio::test]
433 async fn test_request_with_client_identity_reaches_mtls_backend() {
434 crate::server::tls::install_crypto_provider();
435
436 let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
438 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
439 let ca_key = rcgen::KeyPair::generate().unwrap();
440 let ca_cert = ca_params.self_signed(&ca_key).unwrap();
441
442 let server_params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
443 let server_key = rcgen::KeyPair::generate().unwrap();
444 let server_cert = server_params
445 .signed_by(&server_key, &ca_cert, &ca_key)
446 .unwrap();
447
448 let client_params = rcgen::CertificateParams::new(vec!["gw".to_string()]).unwrap();
449 let client_key = rcgen::KeyPair::generate().unwrap();
450 let client_cert = client_params
451 .signed_by(&client_key, &ca_cert, &ca_key)
452 .unwrap();
453
454 let mut roots = rustls::RootCertStore::empty();
456 roots
457 .add(rustls::pki_types::CertificateDer::from(
458 ca_cert.der().to_vec(),
459 ))
460 .unwrap();
461 let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
462 .build()
463 .unwrap();
464 let server_config = rustls::ServerConfig::builder()
465 .with_client_cert_verifier(verifier)
466 .with_single_cert(
467 vec![rustls::pki_types::CertificateDer::from(
468 server_cert.der().to_vec(),
469 )],
470 rustls::pki_types::PrivateKeyDer::try_from(server_key.serialize_der()).unwrap(),
471 )
472 .unwrap();
473 let port = spawn_mtls_server(Arc::new(server_config)).await;
474
475 let dir = std::env::temp_dir();
477 let pid = std::process::id();
478 let cert_path = dir.join(format!("featherbit_ob_mtls_{}.crt", pid));
479 let key_path = dir.join(format!("featherbit_ob_mtls_{}.key", pid));
480 let ca_path = dir.join(format!("featherbit_ob_mtls_{}.ca.crt", pid));
481 std::fs::write(&cert_path, client_cert.pem()).unwrap();
482 std::fs::write(&key_path, client_key.serialize_pem()).unwrap();
483 std::fs::write(&ca_path, ca_cert.pem()).unwrap();
484
485 let identity = tls::UpstreamTls::load(
486 Some((cert_path.to_str().unwrap(), key_path.to_str().unwrap())),
487 Some(ca_path.to_str().unwrap()),
488 true,
489 )
490 .unwrap();
491
492 let client = OutboundClient::new();
493 let ok = client
494 .request(OutboundRequest {
495 method: http::Method::GET,
496 url: format!("https://localhost:{}/", port),
497 headers: Vec::new(),
498 body: Bytes::new(),
499 timeout: Duration::from_secs(5),
500 ssl_verify: true,
501 tls: Some(identity),
502 })
503 .await
504 .expect("mTLS request should succeed");
505 assert_eq!(ok.status, 200);
506
507 let ca_only = tls::UpstreamTls::load(None, Some(ca_path.to_str().unwrap()), true).unwrap();
509 let err = client
510 .request(OutboundRequest {
511 method: http::Method::GET,
512 url: format!("https://localhost:{}/", port),
513 headers: Vec::new(),
514 body: Bytes::new(),
515 timeout: Duration::from_secs(5),
516 ssl_verify: true,
517 tls: Some(ca_only),
518 })
519 .await;
520 assert!(
521 matches!(err, Err(OutboundError::Transport(_))),
522 "got: {:?}",
523 err.map(|r| r.status)
524 );
525 }
526}