Skip to main content

kube_client/client/
config_ext.rs

1use std::sync::Arc;
2
3use http::{HeaderValue, header::HeaderName};
4#[cfg(feature = "openssl-tls")] use hyper::rt::{Read, Write};
5use hyper_util::client::legacy::connect::HttpConnector;
6use jiff::Timestamp;
7use secrecy::ExposeSecret;
8use tower::{filter::AsyncFilterLayer, util::Either};
9
10#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] use super::tls;
11use super::{
12    auth::Auth,
13    middleware::{AddAuthorizationLayer, AuthLayer, BaseUriLayer, ExtraHeadersLayer},
14};
15use crate::{Config, Error, Result};
16
17/// Extensions to [`Config`](crate::Config) for custom [`Client`](crate::Client).
18///
19/// See [`Client::new`](crate::Client::new) for an example.
20///
21/// This trait is sealed and cannot be implemented.
22pub trait ConfigExt: private::Sealed {
23    /// Layer to set the base URI of requests to the configured server.
24    fn base_uri_layer(&self) -> BaseUriLayer;
25
26    /// Optional layer to set up `Authorization` header depending on the config.
27    fn auth_layer(&self) -> Result<Option<AuthLayer>>;
28
29    /// Layer to add non-authn HTTP headers depending on the config.
30    fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer>;
31
32    /// Create [`hyper_rustls::HttpsConnector`] based on config.
33    ///
34    /// # Example
35    ///
36    /// ```rust
37    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
38    /// # use kube::{client::{Body, ConfigExt}, Config};
39    /// # use hyper_util::rt::TokioExecutor;
40    /// let config = Config::infer().await?;
41    /// let https = config.rustls_https_connector()?;
42    /// let hyper_client: hyper_util::client::legacy::Client<_, Body> = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
43    /// # Ok(())
44    /// # }
45    /// ```
46    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
47    #[cfg(feature = "rustls-tls")]
48    fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>>;
49
50    /// Create [`hyper_rustls::HttpsConnector`] based on config and `connector`.
51    ///
52    /// # Example
53    ///
54    /// ```rust
55    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
56    /// # use kube::{client::{Body, ConfigExt}, Config};
57    /// # use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
58    /// let config = Config::infer().await?;
59    /// let mut connector = HttpConnector::new();
60    /// connector.enforce_http(false);
61    /// let https = config.rustls_https_connector_with_connector(connector)?;
62    /// let hyper_client: hyper_util::client::legacy::Client<_, Body> = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
63    /// # Ok(())
64    /// # }
65    /// ```
66    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
67    #[cfg(feature = "rustls-tls")]
68    fn rustls_https_connector_with_connector<H>(
69        &self,
70        connector: H,
71    ) -> Result<hyper_rustls::HttpsConnector<H>>;
72
73    /// Create [`rustls::ClientConfig`] based on config.
74    /// # Example
75    ///
76    /// ```rust
77    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
78    /// # use hyper_util::client::legacy::connect::HttpConnector;
79    /// # use kube::{client::ConfigExt, Config};
80    /// let config = Config::infer().await?;
81    /// let https = {
82    ///     let rustls_config = std::sync::Arc::new(config.rustls_client_config()?);
83    ///     let mut http = HttpConnector::new();
84    ///     http.enforce_http(false);
85    ///     hyper_rustls::HttpsConnector::from((http, rustls_config))
86    /// };
87    /// # Ok(())
88    /// # }
89    /// ```
90    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
91    #[cfg(feature = "rustls-tls")]
92    fn rustls_client_config(&self) -> Result<rustls::ClientConfig>;
93
94    /// Create [`hyper_openssl::HttpsConnector`] based on config.
95    /// # Example
96    ///
97    /// ```rust
98    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
99    /// # use kube::{client::ConfigExt, Config};
100    /// let config = Config::infer().await?;
101    /// let https = config.openssl_https_connector()?;
102    /// # Ok(())
103    /// # }
104    /// ```
105    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
106    #[cfg(feature = "openssl-tls")]
107    fn openssl_https_connector(&self)
108    -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>>;
109
110    /// Create [`hyper_openssl::HttpsConnector`] based on config and `connector`.
111    /// # Example
112    ///
113    /// ```rust
114    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
115    /// # use hyper_util::client::legacy::connect::HttpConnector;
116    /// # use kube::{client::ConfigExt, Config};
117    /// let mut http = HttpConnector::new();
118    /// http.enforce_http(false);
119    /// let config = Config::infer().await?;
120    /// let https = config.openssl_https_connector_with_connector(http)?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
125    #[cfg(feature = "openssl-tls")]
126    fn openssl_https_connector_with_connector<H>(
127        &self,
128        connector: H,
129    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
130    where
131        H: tower::Service<http::Uri> + Send,
132        H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
133        H::Future: Send + 'static,
134        H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin;
135
136    /// Create [`openssl::ssl::SslConnectorBuilder`] based on config.
137    /// # Example
138    ///
139    /// ```rust
140    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
141    /// # use hyper_util::client::legacy::connect::HttpConnector;
142    /// # use kube::{client::ConfigExt, Client, Config};
143    /// let config = Config::infer().await?;
144    /// let https = {
145    ///     let mut http = HttpConnector::new();
146    ///     http.enforce_http(false);
147    ///     let ssl = config.openssl_ssl_connector_builder()?;
148    ///     hyper_openssl::client::legacy::HttpsConnector::with_connector(http, ssl)?
149    /// };
150    /// # Ok(())
151    /// # }
152    /// ```
153    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
154    #[cfg(feature = "openssl-tls")]
155    fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder>;
156}
157
158#[cfg(all(test, feature = "openssl-tls"))]
159mod openssl_tls_server_name_tests {
160    use std::{
161        net::TcpListener,
162        sync::{Arc, Mutex},
163    };
164
165    use openssl::{
166        asn1::Asn1Time,
167        hash::MessageDigest,
168        pkey::{PKey, Private},
169        rsa::Rsa,
170        ssl::{NameType, SslAcceptor, SslMethod},
171        x509::{
172            X509, X509NameBuilder,
173            extension::{BasicConstraints, SubjectAlternativeName},
174        },
175    };
176    use tower::ServiceExt as _;
177
178    use super::*;
179
180    enum San<'a> {
181        Dns(&'a str),
182        Ip(&'a str),
183    }
184
185    // Self-signed cert with a single SAN. A DNS SAN won't match the 127.0.0.1 we connect to, so
186    // verification only passes if the verify-host comes from tls_server_name.
187    fn self_signed_cert(san: San) -> (X509, PKey<Private>) {
188        let cn = match san {
189            San::Dns(s) | San::Ip(s) => s,
190        };
191        let pkey = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
192
193        let mut name = X509NameBuilder::new().unwrap();
194        name.append_entry_by_text("CN", cn).unwrap();
195        let name = name.build();
196
197        let mut builder = X509::builder().unwrap();
198        builder.set_version(2).unwrap();
199        builder.set_subject_name(&name).unwrap();
200        builder.set_issuer_name(&name).unwrap();
201        builder.set_pubkey(&pkey).unwrap();
202        builder
203            .set_not_before(&Asn1Time::days_from_now(0).unwrap())
204            .unwrap();
205        builder
206            .set_not_after(&Asn1Time::days_from_now(1).unwrap())
207            .unwrap();
208        builder
209            .append_extension(BasicConstraints::new().critical().ca().build().unwrap())
210            .unwrap();
211        let mut san_ext = SubjectAlternativeName::new();
212        match san {
213            San::Dns(s) => san_ext.dns(s),
214            San::Ip(s) => san_ext.ip(s),
215        };
216        let san_ext = san_ext.build(&builder.x509v3_context(None, None)).unwrap();
217        builder.append_extension(san_ext).unwrap();
218        builder.sign(&pkey, MessageDigest::sha256()).unwrap();
219
220        (builder.build(), pkey)
221    }
222
223    // Localhost TLS server that records the SNI from the one connection it accepts.
224    fn spawn_tls_server(cert: X509, key: PKey<Private>) -> (u16, Arc<Mutex<Option<String>>>) {
225        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
226        let port = listener.local_addr().unwrap().port();
227        let captured: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
228
229        let captured_in_cb = captured.clone();
230        let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
231        acceptor.set_private_key(&key).unwrap();
232        acceptor.set_certificate(&cert).unwrap();
233        acceptor.set_servername_callback(move |ssl, _alert| {
234            *captured_in_cb.lock().unwrap() = ssl.servername(NameType::HOST_NAME).map(str::to_owned);
235            Ok(())
236        });
237        let acceptor = acceptor.build();
238
239        std::thread::spawn(move || {
240            if let Ok((stream, _)) = listener.accept() {
241                // SNI is captured during the handshake; we don't care whether it then completes.
242                let _ = acceptor.accept(stream);
243            }
244        });
245
246        (port, captured)
247    }
248
249    fn config_for(port: u16, ca: &X509, tls_server_name: Option<&str>) -> Config {
250        let mut config = Config::new(format!("https://127.0.0.1:{port}").parse().unwrap());
251        config.root_cert = Some(vec![ca.to_der().unwrap()]);
252        config.tls_server_name = tls_server_name.map(str::to_owned);
253        config
254    }
255
256    fn connector_for(config: &Config) -> hyper_openssl::client::legacy::HttpsConnector<HttpConnector> {
257        let mut http = HttpConnector::new();
258        http.enforce_http(false);
259        config.openssl_https_connector_with_connector(http).unwrap()
260    }
261
262    // tls_server_name set: SNI carries it (not the 127.0.0.1 host) and verification targets it,
263    // so the handshake against the SAN-only cert succeeds.
264    #[tokio::test]
265    async fn tls_server_name_drives_sni_and_verification() {
266        let server_name = "kubernetes.example.com";
267        let (cert, key) = self_signed_cert(San::Dns(server_name));
268        let (port, captured_sni) = spawn_tls_server(cert.clone(), key);
269
270        let config = config_for(port, &cert, Some(server_name));
271        let uri: http::Uri = config.cluster_url.clone();
272
273        connector_for(&config)
274            .oneshot(uri)
275            .await
276            .expect("handshake should succeed when verification targets tls_server_name");
277
278        assert_eq!(
279            captured_sni.lock().unwrap().as_deref(),
280            Some(server_name),
281            "ClientHello SNI must equal tls_server_name, not the connection host"
282        );
283    }
284
285    // Control: without tls_server_name, verification falls back to the 127.0.0.1 host, which the
286    // SAN-only cert doesn't match, so the handshake fails. Confirms the pass above isn't just lax
287    // verification.
288    #[tokio::test]
289    async fn without_tls_server_name_verification_uses_connection_host() {
290        let server_name = "kubernetes.example.com";
291        let (cert, key) = self_signed_cert(San::Dns(server_name));
292        let (port, _captured_sni) = spawn_tls_server(cert.clone(), key);
293
294        let config = config_for(port, &cert, None);
295        let uri: http::Uri = config.cluster_url.clone();
296
297        let result = connector_for(&config).oneshot(uri).await;
298        assert!(
299            result.is_err(),
300            "handshake must fail when the cert does not match the connection host"
301        );
302    }
303
304    // An IP tls_server_name verifies via set_ip and, per RFC 6066, sends no SNI.
305    #[tokio::test]
306    async fn tls_server_name_as_ip_verifies_without_sni() {
307        let (cert, key) = self_signed_cert(San::Ip("127.0.0.1"));
308        let (port, captured_sni) = spawn_tls_server(cert.clone(), key);
309
310        let config = config_for(port, &cert, Some("127.0.0.1"));
311        let uri: http::Uri = config.cluster_url.clone();
312
313        connector_for(&config)
314            .oneshot(uri)
315            .await
316            .expect("handshake should succeed when the IP tls_server_name matches the cert");
317
318        assert_eq!(
319            *captured_sni.lock().unwrap(),
320            None,
321            "SNI must not be sent for an IP tls_server_name"
322        );
323    }
324
325    // accept_invalid_certs disables verification, so the mismatched cert is accepted.
326    #[tokio::test]
327    async fn accept_invalid_certs_skips_verification() {
328        let (cert, key) = self_signed_cert(San::Dns("kubernetes.example.com"));
329        let (port, _captured_sni) = spawn_tls_server(cert.clone(), key);
330
331        let mut config = config_for(port, &cert, None);
332        config.accept_invalid_certs = true;
333        let uri: http::Uri = config.cluster_url.clone();
334
335        connector_for(&config)
336            .oneshot(uri)
337            .await
338            .expect("handshake should succeed when accept_invalid_certs disables verification");
339    }
340}
341
342mod private {
343    pub trait Sealed {}
344    impl Sealed for super::Config {}
345}
346
347impl ConfigExt for Config {
348    fn base_uri_layer(&self) -> BaseUriLayer {
349        BaseUriLayer::new(self.cluster_url.clone())
350    }
351
352    fn auth_layer(&self) -> Result<Option<AuthLayer>> {
353        Ok(match Auth::try_from(&self.auth_info).map_err(Error::Auth)? {
354            Auth::None => None,
355            Auth::Basic(user, pass) => Some(AuthLayer(Either::Left(
356                AddAuthorizationLayer::basic(&user, pass.expose_secret()).as_sensitive(true),
357            ))),
358            Auth::Bearer(token) => Some(AuthLayer(Either::Left(
359                AddAuthorizationLayer::bearer(token.expose_secret()).as_sensitive(true),
360            ))),
361            Auth::RefreshableToken(refreshable) => {
362                Some(AuthLayer(Either::Right(AsyncFilterLayer::new(refreshable))))
363            }
364            Auth::Certificate(_client_certificate_data, _client_key_data, _) => None,
365        })
366    }
367
368    fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer> {
369        let mut headers = self.headers.clone();
370        if let Some(impersonate_user) = &self.auth_info.impersonate {
371            headers.push((
372                HeaderName::from_static("impersonate-user"),
373                HeaderValue::from_str(impersonate_user)
374                    .map_err(http::Error::from)
375                    .map_err(Error::HttpError)?,
376            ));
377        }
378        if let Some(impersonate_uid) = &self.auth_info.impersonate_uid {
379            headers.push((
380                HeaderName::from_static("impersonate-uid"),
381                HeaderValue::from_str(impersonate_uid)
382                    .map_err(http::Error::from)
383                    .map_err(Error::HttpError)?,
384            ))
385        }
386        if let Some(impersonate_groups) = &self.auth_info.impersonate_groups {
387            for group in impersonate_groups {
388                headers.push((
389                    HeaderName::from_static("impersonate-group"),
390                    HeaderValue::from_str(group)
391                        .map_err(http::Error::from)
392                        .map_err(Error::HttpError)?,
393                ));
394            }
395        }
396        if let Some(impersonate_user_extra) = &self.auth_info.impersonate_user_extra {
397            for (extra_key, extra_values) in impersonate_user_extra {
398                let name = impersonate_extra_header_name(extra_key)?;
399                for value in extra_values {
400                    headers.push((
401                        name.clone(),
402                        HeaderValue::from_str(value)
403                            .map_err(http::Error::from)
404                            .map_err(Error::HttpError)?,
405                    ))
406                }
407            }
408        }
409        Ok(ExtraHeadersLayer {
410            headers: Arc::new(headers),
411        })
412    }
413
414    #[cfg(feature = "rustls-tls")]
415    fn rustls_client_config(&self) -> Result<rustls::ClientConfig> {
416        let identity = match self.exec_identity_pem().0 {
417            Some(identity) => Some(identity),
418            None => self.identity_pem()?,
419        };
420        let mut config = tls::rustls_tls::rustls_client_config(
421            identity.as_deref(),
422            self.root_cert.as_deref(),
423            self.accept_invalid_certs,
424        )
425        .map_err(Error::RustlsTls)?;
426
427        // When a CA file path is available (in-cluster), install a verifier
428        // that re-reads it periodically to survive CA rotation. `root_cert`
429        // bytes are still passed above so the builder typestate is satisfied,
430        // but verification is handed over here.
431        if !self.accept_invalid_certs
432            && let Some(path) = &self.root_cert_file
433        {
434            let verifier = tls::rustls_tls::ReloadingVerifier::new(path.clone()).map_err(Error::RustlsTls)?;
435            config.dangerous().set_certificate_verifier(Arc::new(verifier));
436        }
437        Ok(config)
438    }
439
440    #[cfg(feature = "rustls-tls")]
441    fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>> {
442        let mut connector = HttpConnector::new();
443        connector.enforce_http(false);
444        self.rustls_https_connector_with_connector(connector)
445    }
446
447    #[cfg(feature = "rustls-tls")]
448    fn rustls_https_connector_with_connector<H>(
449        &self,
450        connector: H,
451    ) -> Result<hyper_rustls::HttpsConnector<H>> {
452        use hyper_rustls::FixedServerNameResolver;
453
454        use crate::client::tls::rustls_tls;
455
456        let rustls_config = self.rustls_client_config()?;
457        let mut builder = hyper_rustls::HttpsConnectorBuilder::new()
458            .with_tls_config(rustls_config)
459            .https_or_http();
460        if let Some(tsn) = self.tls_server_name.as_ref() {
461            builder = builder.with_server_name_resolver(FixedServerNameResolver::new(
462                tsn.clone()
463                    .try_into()
464                    .map_err(rustls_tls::Error::InvalidServerName)
465                    .map_err(Error::RustlsTls)?,
466            ));
467        }
468        Ok(builder.enable_http1().wrap_connector(connector))
469    }
470
471    #[cfg(feature = "openssl-tls")]
472    fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder> {
473        let identity = match self.exec_identity_pem().0 {
474            Some(identity) => Some(identity),
475            None => self.identity_pem()?,
476        };
477
478        // tls_server_name has no hook on the builder; it is applied per-connection in
479        // openssl_https_connector_with_connector instead.
480        tls::openssl_tls::ssl_connector_builder(identity.as_ref(), self.root_cert.as_ref())
481            .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e)))
482    }
483
484    #[cfg(feature = "openssl-tls")]
485    fn openssl_https_connector(
486        &self,
487    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>> {
488        let mut connector = HttpConnector::new();
489        connector.enforce_http(false);
490        self.openssl_https_connector_with_connector(connector)
491    }
492
493    #[cfg(feature = "openssl-tls")]
494    fn openssl_https_connector_with_connector<H>(
495        &self,
496        connector: H,
497    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
498    where
499        H: tower::Service<http::Uri> + Send,
500        H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
501        H::Future: Send + 'static,
502        H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin,
503    {
504        let mut https = hyper_openssl::client::legacy::HttpsConnector::with_connector(
505            connector,
506            self.openssl_ssl_connector_builder()?,
507        )
508        .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?;
509        // OpenSSL has no server-name-resolver hook, so unlike rustls we apply tls_server_name in
510        // the per-connection callback (which already exists for accept_invalid_certs).
511        let accept_invalid_certs = self.accept_invalid_certs;
512        let tls_server_name = self.tls_server_name.clone();
513        if accept_invalid_certs || tls_server_name.is_some() {
514            https.set_callback(move |ssl, _uri| {
515                if accept_invalid_certs {
516                    ssl.set_verify(openssl::ssl::SslVerifyMode::NONE);
517                }
518                if let Some(name) = &tls_server_name {
519                    use std::net::IpAddr;
520
521                    use openssl::x509::verify::X509CheckFlags;
522                    // into_ssl(host) runs after this callback and would otherwise set SNI and the
523                    // verify host from the URI host. Disable both so it keeps our values.
524                    ssl.set_use_server_name_indication(false);
525                    ssl.set_verify_hostname(false);
526                    // SNI is not sent for IP literals.
527                    if name.parse::<IpAddr>().is_err() {
528                        ssl.set_hostname(name)?;
529                    }
530                    let param = ssl.param_mut();
531                    param.set_hostflags(X509CheckFlags::NO_PARTIAL_WILDCARDS);
532                    match name.parse::<IpAddr>() {
533                        Ok(ip) => param.set_ip(ip)?,
534                        Err(_) => param.set_host(name)?,
535                    }
536                }
537                Ok(())
538            });
539        }
540        Ok(https)
541    }
542}
543
544impl Config {
545    // This is necessary to retrieve an identity when an exec plugin
546    // returns a client certificate and key instead of a token.
547    // This has be to be checked on TLS configuration vs tokens
548    // which can be added in as an AuthLayer.
549    pub(crate) fn exec_identity_pem(&self) -> (Option<Vec<u8>>, Option<Timestamp>) {
550        match Auth::try_from(&self.auth_info) {
551            Ok(Auth::Certificate(client_certificate_data, client_key_data, expiration)) => {
552                const NEW_LINE: u8 = b'\n';
553
554                let mut buffer = client_key_data.expose_secret().as_bytes().to_vec();
555                buffer.push(NEW_LINE);
556                buffer.extend_from_slice(client_certificate_data.as_bytes());
557                buffer.push(NEW_LINE);
558                (Some(buffer), expiration)
559            }
560            _ => (None, None),
561        }
562    }
563}
564
565/// Generate the appropriate header name for an `impersonate_user_extra` key.
566///
567/// Percent-escapes extra keys so that they are valid header names, using the same percent-escape
568/// set as the Go client (illegal header name bytes and '%').
569fn impersonate_extra_header_name(key: &str) -> Result<HeaderName> {
570    // Source: https://github.com/kubernetes/kubernetes/blob/8f5bbad3e20a347aaae13afd78cc955e642c68e4/staging/src/k8s.io/client-go/transport/round_trippers.go#L714
571    // Note that we don't remove '%' here, since we do want to escape it.
572    const NEEDS_ESCAPE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
573        .remove(b'!')
574        .remove(b'#')
575        .remove(b'$')
576        .remove(b'&')
577        .remove(b'\'')
578        .remove(b'*')
579        .remove(b'+')
580        .remove(b'-')
581        .remove(b'.')
582        .remove(b'^')
583        .remove(b'_')
584        .remove(b'`')
585        .remove(b'|')
586        .remove(b'~');
587
588    let encoded_key = percent_encoding::percent_encode(key.as_bytes(), &NEEDS_ESCAPE);
589    format!("impersonate-extra-{encoded_key}")
590        .try_into()
591        .map_err(http::Error::from)
592        .map_err(Error::HttpError)
593}
594
595#[cfg(test)]
596mod tests {
597    use super::impersonate_extra_header_name;
598
599    #[test]
600    fn impersonate_user_extra_no_unnecessary_escape() {
601        assert_eq!(
602            impersonate_extra_header_name("FOO-bar.baz_123").unwrap().as_str(),
603            "impersonate-extra-foo-bar.baz_123"
604        );
605    }
606
607    #[test]
608    fn impersonate_user_extra_escapes_percent() {
609        assert_eq!(
610            impersonate_extra_header_name("foo%2f").unwrap().as_str(),
611            "impersonate-extra-foo%252f"
612        );
613    }
614
615    #[test]
616    fn impersonate_user_extra_escapes_slash() {
617        assert_eq!(
618            impersonate_extra_header_name("foo.com/bar").unwrap().as_str(),
619            "impersonate-extra-foo.com%2fbar"
620        );
621    }
622
623    #[test]
624    fn impersonate_user_extra_roundtrips() {
625        for chr in 0..128 {
626            let key = str::from_utf8(&[chr]).unwrap().to_lowercase();
627            let encoded = impersonate_extra_header_name(&key).unwrap();
628            let decoded = percent_encoding::percent_decode_str(encoded.as_str())
629                .decode_utf8()
630                .unwrap();
631
632            assert_eq!(format!("impersonate-extra-{key}"), decoded);
633        }
634    }
635}