1use bytes::Bytes;
2use http::{Request, Response, header::HeaderMap};
3use hyper::{
4 body::Incoming,
5 rt::{Read, Write},
6};
7use hyper_timeout::TimeoutConnector;
8
9use hyper_util::{
10 client::legacy::connect::{Connection, HttpConnector},
11 rt::TokioExecutor,
12};
13
14use jiff::Timestamp;
15use std::{pin::Pin, time::Duration};
16use tower::{BoxError, Layer, Service, ServiceBuilder, ServiceExt as _, retry::RetryLayer, util::BoxService};
17use tower_http::{ServiceExt as _, classify::ServerErrorsFailureClass, trace::TraceLayer};
18use tracing::Span;
19
20use super::body::Body;
21use crate::{Client, Config, Error, Result, client::{ConfigExt, retry::RetryPolicy}};
22
23pub type DynBody = dyn http_body::Body<Data = Bytes, Error = BoxError> + Send + Unpin;
27
28pub struct ClientBuilder<Svc> {
30 service: Svc,
31 default_ns: String,
32 valid_until: Option<Timestamp>,
33}
34
35impl<Svc> ClientBuilder<Svc> {
36 pub fn new(service: Svc, default_namespace: impl Into<String>) -> Self
41 where
42 Svc: Service<Request<Body>>,
43 {
44 Self {
45 service,
46 default_ns: default_namespace.into(),
47 valid_until: None,
48 }
49 }
50
51 pub fn with_layer<L: Layer<Svc>>(self, layer: &L) -> ClientBuilder<L::Service> {
53 let Self {
54 service: stack,
55 default_ns,
56 valid_until,
57 } = self;
58 ClientBuilder {
59 service: layer.layer(stack),
60 default_ns,
61 valid_until,
62 }
63 }
64
65 pub fn with_valid_until(self, valid_until: Option<Timestamp>) -> Self {
67 ClientBuilder {
68 service: self.service,
69 default_ns: self.default_ns,
70 valid_until,
71 }
72 }
73
74 pub fn build<B>(self) -> Client
76 where
77 Svc: Service<Request<Body>, Response = Response<B>> + Send + 'static,
78 Svc::Future: Send + 'static,
79 Svc::Error: Into<BoxError>,
80 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
81 B::Error: Into<BoxError>,
82 {
83 Client::new(self.service, self.default_ns).with_valid_until(self.valid_until)
84 }
85}
86
87pub type GenericService = BoxService<Request<Body>, Response<Box<DynBody>>, BoxError>;
88
89#[cfg(feature = "http-proxy")]
97fn proxy_basic_auth_value(userinfo: &str) -> String {
98 use base64::Engine;
99
100 format!(
101 "Basic {}",
102 base64::engine::general_purpose::STANDARD
103 .encode(percent_encoding::percent_decode_str(userinfo).collect::<Vec<u8>>())
104 )
105}
106
107#[cfg(feature = "http-proxy")]
108fn with_proxy_basic_auth<C>(
109 proxy_url: &http::Uri,
110 mut connector: hyper_util::client::legacy::connect::proxy::Tunnel<C>,
111) -> hyper_util::client::legacy::connect::proxy::Tunnel<C> {
112 if let Some(authority) = proxy_url.authority()
113 && let Some((userinfo, _)) = authority.as_str().split_once('@')
114 {
115 use http::HeaderValue;
116
117 if let Ok(header) = HeaderValue::from_str(&proxy_basic_auth_value(userinfo)) {
118 connector = connector.with_auth(header);
119 }
120 }
121
122 connector
123}
124
125#[cfg(all(test, feature = "http-proxy"))]
126mod proxy_auth_tests {
127 use super::proxy_basic_auth_value;
128 use base64::Engine;
129
130 fn basic_auth(plain: &str) -> String {
131 format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(plain))
132 }
133
134 #[test]
135 fn decodes_percent_escapes_before_basic_auth_encoding() {
136 assert_eq!(
138 proxy_basic_auth_value("user:pa%40ss%3Aword"),
139 basic_auth("user:pa@ss:word")
140 );
141 }
142
143 #[test]
144 fn leaves_plain_userinfo_unchanged() {
145 assert_eq!(proxy_basic_auth_value("user:pass"), basic_auth("user:pass"));
146 }
147
148 #[test]
149 fn passes_through_invalid_escape_with_sign_prefix() {
150 assert_eq!(
154 proxy_basic_auth_value("user:pass%+A"),
155 basic_auth("user:pass%+A")
156 );
157 }
158}
159
160impl TryFrom<Config> for ClientBuilder<GenericService> {
161 type Error = Error;
162
163 fn try_from(config: Config) -> Result<Self> {
183 let executor = TokioExecutor::new();
184 Self::try_from((config, executor))
185 }
186}
187
188impl<E> TryFrom<(Config, E)> for ClientBuilder<GenericService>
189where
190 E: hyper::rt::Executor<Pin<Box<dyn Future<Output = ()> + Send>>> + Send + Sync + Clone + 'static,
191{
192 type Error = Error;
193
194 fn try_from((config, executor): (Config, E)) -> Result<Self> {
217 let mut connector = HttpConnector::new();
218 connector.enforce_http(false);
219
220 #[cfg(all(feature = "aws-lc-rs", feature = "rustls-tls"))]
221 {
222 if rustls::crypto::CryptoProvider::get_default().is_none() {
223 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
226 }
227 }
228
229 match config.proxy_url.as_ref() {
230 Some(proxy_url) if proxy_url.scheme_str() == Some("socks5") => {
231 #[cfg(feature = "socks5")]
232 {
233 let connector = hyper_util::client::legacy::connect::proxy::SocksV5::new(
234 proxy_url.clone(),
235 connector,
236 );
237 make_generic_builder(executor, connector, config)
238 }
239
240 #[cfg(not(feature = "socks5"))]
241 Err(Error::ProxyProtocolDisabled {
242 proxy_url: Box::new(proxy_url.clone()),
243 protocol_feature: "kube/socks5",
244 })
245 }
246
247 Some(proxy_url) if proxy_url.scheme_str() == Some("http") => {
248 #[cfg(feature = "http-proxy")]
249 {
250 let connector =
251 hyper_util::client::legacy::connect::proxy::Tunnel::new(proxy_url.clone(), connector);
252 let connector = with_proxy_basic_auth(proxy_url, connector);
253
254 make_generic_builder(executor, connector, config)
255 }
256
257 #[cfg(not(feature = "http-proxy"))]
258 Err(Error::ProxyProtocolDisabled {
259 proxy_url: Box::new(proxy_url.clone()),
260 protocol_feature: "kube/http-proxy",
261 })
262 }
263
264 Some(proxy_url) if proxy_url.scheme_str() == Some("https") => {
265 #[cfg(all(feature = "http-proxy", any(feature = "rustls-tls", feature = "openssl-tls")))]
266 {
267 #[cfg(feature = "rustls-tls")]
268 let proxy_connector = config.rustls_https_connector_with_connector(connector)?;
269 #[cfg(all(not(feature = "rustls-tls"), feature = "openssl-tls"))]
270 let proxy_connector = config.openssl_https_connector_with_connector(connector)?;
271
272 let connector =
273 hyper_util::client::legacy::connect::proxy::Tunnel::new(proxy_url.clone(), proxy_connector);
274 let connector = with_proxy_basic_auth(proxy_url, connector);
275
276 make_generic_builder(executor, connector, config)
277 }
278
279 #[cfg(all(feature = "http-proxy", not(any(feature = "rustls-tls", feature = "openssl-tls"))))]
280 return Err(Error::TlsRequired);
281
282 #[cfg(not(feature = "http-proxy"))]
283 Err(Error::ProxyProtocolDisabled {
284 proxy_url: Box::new(proxy_url.clone()),
285 protocol_feature: "kube/http-proxy",
286 })
287 }
288
289 Some(proxy_url) => Err(Error::ProxyProtocolUnsupported {
290 proxy_url: Box::new(proxy_url.clone()),
291 }),
292
293 None => make_generic_builder(executor, connector, config),
294 }
295 }
296}
297
298fn make_generic_builder<E, H>(
301 executor: E,
302 connector: H,
303 config: Config,
304) -> Result<ClientBuilder<GenericService>, Error>
305where
306 H: 'static + Clone + Send + Sync + Service<http::Uri>,
307 H::Response: 'static + Connection + Read + Write + Send + Unpin,
308 H::Future: 'static + Send,
309 H::Error: 'static + Send + Sync + std::error::Error,
310 E: hyper::rt::Executor<Pin<Box<dyn Future<Output = ()> + Send>>> + Send + Sync + Clone + 'static,
311{
312 let default_ns = config.default_namespace.clone();
313 let auth_layer = config.auth_layer()?;
314
315 let client: hyper_util::client::legacy::Client<_, Body> = {
316 #[cfg(feature = "rustls-tls")]
322 let connector = config.rustls_https_connector_with_connector(connector)?;
323 #[cfg(all(not(feature = "rustls-tls"), feature = "openssl-tls"))]
324 let connector = config.openssl_https_connector_with_connector(connector)?;
325 #[cfg(all(not(feature = "rustls-tls"), not(feature = "openssl-tls")))]
326 if config.cluster_url.scheme() == Some(&http::uri::Scheme::HTTPS) {
327 return Err(Error::TlsRequired);
329 }
330
331 let mut connector = TimeoutConnector::new(connector);
332
333 connector.set_connect_timeout(config.connect_timeout);
335 connector.set_read_timeout(config.read_timeout);
336 connector.set_write_timeout(config.write_timeout);
337 connector.set_reset_reader_on_write(config.reset_reader_on_write);
338
339 hyper_util::client::legacy::Builder::new(executor).build(connector)
340 };
341
342 let stack = ServiceBuilder::new().layer(config.base_uri_layer()).into_inner();
343 #[cfg(feature = "gzip")]
344 let stack = ServiceBuilder::new()
345 .layer(stack)
346 .layer(
347 tower_http::decompression::DecompressionLayer::new()
348 .no_br()
349 .no_deflate()
350 .no_zstd()
351 .gzip(!config.disable_compression),
352 )
353 .into_inner();
354
355 let service = ServiceBuilder::new()
356 .layer(stack)
357 .option_layer(config.default_retry.then_some(RetryLayer::new(RetryPolicy::server_retry())))
358 .option_layer(auth_layer)
359 .layer(config.extra_headers_layer()?)
360 .layer(
361 TraceLayer::new_for_http()
364 .make_span_with(|req: &Request<Body>| {
365 tracing::debug_span!(
366 "HTTP",
367 http.method = %req.method(),
368 http.url = %req.uri(),
369 http.status_code = tracing::field::Empty,
370 otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"),
371 otel.kind = "client",
372 otel.status_code = tracing::field::Empty,
373 )
374 })
375 .on_request(|_req: &Request<Body>, _span: &Span| {
376 tracing::debug!("requesting");
377 })
378 .on_response(|res: &Response<Incoming>, _latency: Duration, span: &Span| {
379 let status = res.status();
380 span.record("http.status_code", status.as_u16());
381 if status.is_client_error() || status.is_server_error() {
382 span.record("otel.status_code", "ERROR");
383 }
384 })
385 .on_body_chunk(())
387 .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| {
388 tracing::debug!("stream closed");
389 })
390 .on_failure(|ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| {
391 span.record("otel.status_code", "ERROR");
397 match ec {
398 ServerErrorsFailureClass::StatusCode(status) => {
399 span.record("http.status_code", status.as_u16());
400 tracing::error!("failed with status {}", status)
401 }
402 ServerErrorsFailureClass::Error(err) => {
403 tracing::error!("failed with error {}", err)
404 }
405 }
406 }),
407 )
408 .map_err(BoxError::from)
409 .service(client);
410
411 let (_, expiration) = config.exec_identity_pem();
412
413 let client = ClientBuilder::new(
414 service
415 .map_response_body(|body| {
416 Box::new(http_body_util::BodyExt::map_err(body, BoxError::from)) as Box<DynBody>
417 })
418 .boxed(),
419 default_ns,
420 )
421 .with_valid_until(expiration);
422
423 Ok(client)
424}
425
426#[cfg(test)]
427mod tests {
428 #[cfg(feature = "gzip")] use super::*;
429
430 #[cfg(all(feature = "gzip", feature = "rustls-tls"))]
431 #[tokio::test]
432 async fn test_no_accept_encoding_header_sent_when_compression_disabled()
433 -> Result<(), Box<dyn std::error::Error>> {
434 use http::Uri;
435 use std::net::SocketAddr;
436 use tokio::net::{TcpListener, TcpStream};
437
438 #[cfg(feature = "aws-lc-rs")]
440 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
441
442 let addr: SocketAddr = ([127, 0, 0, 1], 0).into();
444 let listener = TcpListener::bind(addr).await?;
445 let local_addr = listener.local_addr()?;
446 let uri: Uri = format!("http://{}", local_addr).parse()?;
447
448 tokio::spawn(async move {
449 use http_body_util::Full;
450 use hyper::{server::conn::http1, service::service_fn};
451 use hyper_util::rt::{TokioIo, TokioTimer};
452 use std::convert::Infallible;
453
454 loop {
455 let (tcp, _) = listener.accept().await.unwrap();
456 let io: TokioIo<TcpStream> = TokioIo::new(tcp);
457
458 tokio::spawn(async move {
459 http1::Builder::new()
460 .timer(TokioTimer::new())
461 .serve_connection(
462 io,
463 service_fn(|req| async move {
464 let response = req
465 .headers()
466 .get(http::header::ACCEPT_ENCODING)
467 .map(|b| Bytes::copy_from_slice(b.as_bytes()))
468 .unwrap_or_default();
469 Ok::<_, Infallible>(Response::new(Full::new(response)))
470 }),
471 )
472 .await
473 .unwrap();
474 });
475 }
476 });
477
478 let config = Config { ..Config::new(uri) };
480 let client =
481 make_generic_builder(TokioExecutor::new(), HttpConnector::new(), config.clone())?.build();
482 let response = client.request_text(http::Request::default()).await?;
483 assert_eq!(&response, "gzip");
484
485 let config = Config {
487 disable_compression: true,
488 ..config
489 };
490 let client = make_generic_builder(TokioExecutor::new(), HttpConnector::new(), config)?.build();
491 let response = client.request_text(http::Request::default()).await?;
492 assert_eq!(&response, "");
493
494 Ok(())
495 }
496}