Skip to main content

mz_ccsr/
config.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeMap;
11use std::fmt;
12use std::net::SocketAddr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use serde::{Deserialize, Serialize};
17use url::Url;
18
19use crate::client::Client;
20use crate::tls::{Certificate, Identity};
21
22#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
23pub struct Auth {
24    pub username: String,
25    pub password: Option<String>,
26}
27
28/// Configuration for a `Client`.
29#[derive(Clone)]
30pub struct ClientConfig {
31    url: Arc<dyn Fn() -> Url + Send + Sync + 'static>,
32    root_certs: Vec<Certificate>,
33    identity: Option<Identity>,
34    auth: Option<Auth>,
35    dns_overrides: BTreeMap<String, Vec<SocketAddr>>,
36}
37
38impl fmt::Debug for ClientConfig {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.debug_struct("ClientConfig")
41            .field("url", &"...")
42            .field("root_certs", &self.root_certs)
43            .field("identity", &self.identity)
44            .field("auth", &self.auth)
45            .field("dns_overrides", &self.dns_overrides)
46            .finish()
47    }
48}
49
50impl ClientConfig {
51    /// Constructs a new `ClientConfig` that will target the schema registry at
52    /// the specified URL.
53    pub fn new(url: Url) -> ClientConfig {
54        ClientConfig {
55            url: Arc::new(move || url.clone()),
56            root_certs: Vec::new(),
57            identity: None,
58            auth: None,
59            dns_overrides: BTreeMap::new(),
60        }
61    }
62
63    /// Adds a trusted root TLS certificate.
64    ///
65    /// Certificates in the system's certificate store are trusted by default.
66    /// A server certificate identical to `cert` is trusted even if it is not
67    /// a valid end-entity certificate, for example a self-signed CA. Its name
68    /// and validity period are still checked, and its name may match any
69    /// subject common name if it has no DNS or IP subjectAltName.
70    pub fn add_root_certificate(mut self, cert: Certificate) -> ClientConfig {
71        self.root_certs.push(cert);
72        self
73    }
74
75    /// Enables TLS client authentication with the provided identity.
76    pub fn identity(mut self, identity: Identity) -> ClientConfig {
77        self.identity = Some(identity);
78        self
79    }
80
81    /// Enables HTTP basic authentication with the specified username and
82    /// optional password.
83    pub fn auth(mut self, username: String, password: Option<String>) -> ClientConfig {
84        self.auth = Some(Auth { username, password });
85        self
86    }
87
88    /// Overrides DNS resolution for specific domains to the provided IP
89    /// addresses.
90    ///
91    /// See [`reqwest::ClientBuilder::resolve_to_addrs`].
92    pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientConfig {
93        self.dns_overrides.insert(domain.into(), addrs.into());
94        self
95    }
96
97    /// Sets a callback that will be used to dynamically override the url
98    /// the client uses.
99    // Note this this doesn't use native `reqwest` `Proxy`s because not all schema
100    // registry implementations support them.
101    pub fn dynamic_url<F: Fn() -> Url + Send + Sync + 'static>(
102        mut self,
103        callback: F,
104    ) -> ClientConfig {
105        self.url = Arc::new(callback);
106        self
107    }
108
109    /// Builds the [`Client`].
110    pub fn build(self) -> Result<Client, anyhow::Error> {
111        let mut builder = reqwest::ClientBuilder::new();
112
113        // NOTE: A preconfigured TLS backend makes reqwest ignore all of its TLS
114        // builder settings (roots, identity, ALPN, SNI, TLS versions, CRLs), so
115        // any such setting must go into `rustls_config` instead.
116        if !self.root_certs.is_empty() {
117            let tls = crate::tls::rustls_config(&self.root_certs, self.identity.as_ref())?;
118            builder = builder.tls_backend_preconfigured(tls);
119        } else if let Some(ident) = self.identity {
120            builder = builder.identity(ident.into());
121        }
122
123        for (domain, addrs) in self.dns_overrides {
124            builder = builder.resolve_to_addrs(&domain, &addrs);
125        }
126
127        // TODO(guswynn): make this configurable.
128        let timeout = Duration::from_secs(60);
129
130        let inner = builder
131            .redirect(reqwest::redirect::Policy::none())
132            .timeout(timeout)
133            .build()
134            .map_err(|e| {
135                // Use {e:#} to include the full error chain (not just the top-level "builder error").
136                anyhow::anyhow!("failed to build schema registry HTTP client: {e:#}")
137            })?;
138
139        Client::new(inner, self.url, self.auth, timeout)
140    }
141}