dex_sdk/client/
flow_service_tls.rs1use std::error::Error;
10use std::net::{IpAddr, Ipv6Addr};
11
12use rustls::RootCertStore;
13use rustls::pki_types::{CertificateDer, pem::PemObject};
14use tonic::transport::{ClientTlsConfig, Endpoint};
15
16use crate::{SdkError, SdkResult, ServiceError};
17
18#[derive(Clone, Default)]
57pub struct FlowServiceTlsOptions {
58 additional_root_cas_pem: Option<Vec<u8>>,
59 server_name: Option<String>,
60}
61
62impl FlowServiceTlsOptions {
63 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn additional_root_cas_pem(mut self, value: impl Into<Vec<u8>>) -> Self {
81 self.additional_root_cas_pem = Some(value.into());
82 self
83 }
84
85 pub fn server_name(mut self, value: impl Into<String>) -> Self {
96 self.server_name = Some(value.into());
97 self
98 }
99}
100
101impl std::fmt::Debug for FlowServiceTlsOptions {
102 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 formatter
104 .debug_struct("FlowServiceTlsOptions")
105 .field(
106 "additional_root_cas_pem_bytes",
107 &self.additional_root_cas_pem.as_ref().map(Vec::len),
108 )
109 .field("server_name", &self.server_name)
110 .finish()
111 }
112}
113
114pub(crate) fn flow_service_endpoint(
115 address: &str,
116 options: Option<&FlowServiceTlsOptions>,
117 operation: &'static str,
118) -> SdkResult<Endpoint> {
119 let Some(options) = options else {
120 let address = if address.contains("://") {
121 address.to_string()
122 } else {
123 format!("http://{address}")
124 };
125 return Endpoint::from_shared(address)
126 .map_err(|error| transport_error(operation, "build FlowService endpoint", error));
127 };
128 let endpoint_address = tls_endpoint_address(address)?;
129 let (host, _) = endpoint_address
130 .rsplit_once(':')
131 .expect("validated TLS endpoint has a port");
132 let host = host.trim_start_matches('[').trim_end_matches(']');
133 let mut tls = ClientTlsConfig::new().domain_name(host);
134 let mut roots = RootCertStore::empty();
135 if let Some(server_name) = &options.server_name {
136 if !is_valid_tls_name(server_name)
137 || rustls::pki_types::ServerName::try_from(server_name.as_str()).is_err()
138 {
139 return Err(invalid_tls(
140 "flow_service_tls.server_name must be a nonempty DNS name or IP address without a port",
141 ));
142 }
143 tls = tls.domain_name(server_name);
144 }
145 let mut skipped_added_count = 0;
146 if let Some(bundle) = &options.additional_root_cas_pem {
147 let (added_count, skipped_count) = add_additional_certificates(&mut roots, bundle);
148 skipped_added_count = skipped_count;
149 if !bundle.is_empty() && added_count == 0 {
150 return Err(invalid_tls(
151 "flow_service_tls.additional_root_cas_pem contains no PEM certificate",
152 ));
153 }
154 }
155 let mut endpoint = Endpoint::from_shared(format!("https://{endpoint_address}"))
156 .map_err(|error| transport_error(operation, "build FlowService endpoint", error))?;
157 if let Some(server_name) = &options.server_name {
158 let authority = if server_name.parse::<Ipv6Addr>().is_ok() {
159 format!("[{server_name}]")
160 } else {
161 server_name.clone()
162 };
163 endpoint = endpoint.origin(format!("https://{authority}").parse().map_err(|_| {
164 invalid_tls("flow_service_tls.server_name is not a valid HTTP/2 authority")
165 })?);
166 }
167 let native = rustls_native_certs::load_native_certs();
168 if !native.errors.is_empty() {
169 tracing::warn!(
170 error_count = native.errors.len(),
171 "failed to load some native root CAs"
172 );
173 }
174 let (_, skipped_count) = roots.add_parsable_certificates(native.certs);
175 if skipped_count > 0 {
176 tracing::debug!(skipped_count, "skipped invalid native root CAs");
177 }
178 if skipped_added_count > 0 {
179 tracing::warn!(
180 skipped_count = skipped_added_count,
181 "skipped invalid additional root CA PEM blocks"
182 );
183 }
184 if roots.is_empty() {
185 tracing::warn!("FlowService TLS trust store is empty; server verification will fail");
186 }
187 endpoint
188 .tls_config(tls.trust_anchors(roots.roots))
189 .map_err(|error| transport_error(operation, "build FlowService TLS connector", error))
190}
191
192fn tls_endpoint_address(address: &str) -> SdkResult<&str> {
193 let unsupported = || {
194 invalid_tls(
195 "flow_service_tls requires server_address as host:port, dns:///host:port, or dns://authority/host:port",
196 )
197 };
198 let endpoint = if let Some((scheme, target)) = address.split_once("://") {
199 if !scheme.eq_ignore_ascii_case("dns") {
200 return Err(unsupported());
201 }
202 let (authority, endpoint) = target.split_once('/').ok_or_else(unsupported)?;
203 if authority.contains([' ', '\t', '\r', '\n', '?', '#', '@']) {
204 return Err(unsupported());
205 }
206 endpoint
207 } else {
208 address
209 };
210 if endpoint.contains(['/', '?', '#']) {
211 return Err(unsupported());
212 }
213 let (host, port) = endpoint.rsplit_once(':').ok_or_else(unsupported)?;
214 let host = if let Some(bracketed_host) = host.strip_prefix('[') {
215 let ipv6 = bracketed_host.strip_suffix(']').ok_or_else(unsupported)?;
216 ipv6.parse::<Ipv6Addr>().map_err(|_| unsupported())?;
217 ipv6
218 } else {
219 if host.contains([':', '[', ']']) {
220 return Err(unsupported());
221 }
222 host
223 };
224 if rustls::pki_types::ServerName::try_from(host).is_err() {
225 return Err(invalid_tls(
226 "flow_service_tls server_address host is not a valid DNS name or IP address",
227 ));
228 }
229 if !is_valid_tls_name(host)
230 || [
231 "dns",
232 "unix",
233 "unix-abstract",
234 "passthrough",
235 "ipv4",
236 "ipv6",
237 ]
238 .contains(&host.to_ascii_lowercase().as_str())
239 {
240 return Err(unsupported());
241 }
242 if port.is_empty()
243 || !port.bytes().all(|value| value.is_ascii_digit())
244 || !port.parse::<u16>().is_ok_and(|value| value > 0)
245 {
246 return Err(invalid_tls(
247 "flow_service_tls requires server_address port 1-65535",
248 ));
249 }
250 Ok(endpoint)
251}
252
253fn is_valid_tls_name(name: &str) -> bool {
254 !name.is_empty()
255 && !name.contains([' ', '\t', '\r', '\n', '/', '#', '?', '@', '[', ']'])
256 && (!name.contains(':') || name.parse::<IpAddr>().is_ok())
257}
258
259fn add_additional_certificates(roots: &mut RootCertStore, bundle: &[u8]) -> (usize, usize) {
260 let bundle = String::from_utf8_lossy(bundle);
261 let mut added_count = 0;
262 let mut skipped_count = 0;
263 for block in bundle.split("-----BEGIN CERTIFICATE-----").skip(1) {
264 let Some((body, _)) = block.split_once("-----END CERTIFICATE-----") else {
265 skipped_count += 1;
266 continue;
267 };
268 let certificate = format!("-----BEGIN CERTIFICATE-----{body}-----END CERTIFICATE-----");
269 match CertificateDer::from_pem_slice(certificate.as_bytes()) {
270 Ok(parsed) => match roots.add(parsed.into_owned()) {
271 Ok(()) => added_count += 1,
272 Err(_) => skipped_count += 1,
273 },
274 Err(_) => skipped_count += 1,
275 }
276 }
277 (added_count, skipped_count)
278}
279
280fn invalid_tls(message: &str) -> SdkError {
281 SdkError::InvalidArgument {
282 message: message.to_string(),
283 }
284}
285
286fn transport_error(operation: &'static str, object: &str, error: impl Error + 'static) -> SdkError {
287 let mut detail = format!("{object}: {error}");
288 let mut source = error.source();
289 while let Some(cause) = source {
290 detail.push_str(": ");
291 detail.push_str(&cause.to_string());
292 source = cause.source();
293 }
294 SdkError::Service {
295 service: ServiceError::local(operation, detail),
296 }
297}