1#[cfg(feature = "grpc-tls")]
4use anyhow::Context;
5use anyhow::Result;
6use architect_api::HumanDuration;
7use clap::Args;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10#[cfg(feature = "grpc-tls")]
11use tonic::transport::{Certificate, ClientTlsConfig};
12use tonic::transport::{Channel, Endpoint};
13use url::Url;
14
15#[cfg(feature = "grpc-tls")]
16const ARCHITECT_CA: &[u8] = include_bytes!("ca.crt");
17
18#[derive(Debug, Clone, Serialize, Deserialize, Args)]
19pub struct GrpcClientConfig {
20 #[serde(default)]
22 #[arg(long)]
23 pub connect_timeout: Option<HumanDuration>,
24 #[serde(default)]
25 #[arg(skip)]
26 pub connect_lazy: bool,
27 #[serde(default)]
31 #[arg(long)]
32 pub tls_client: bool,
33 #[serde(default)]
37 #[arg(long)]
38 pub tls_identity: Option<PathBuf>,
39 #[serde(default)]
42 #[arg(long)]
43 pub tls_identity_key: Option<PathBuf>,
44 #[serde(default)]
48 #[arg(long = "ca")]
49 pub dangerous_additional_ca_certificate: Option<PathBuf>,
50}
51
52impl Default for GrpcClientConfig {
53 fn default() -> Self {
54 Self {
55 connect_timeout: Self::default_connect_timeout(),
56 connect_lazy: false,
57 tls_client: false,
58 tls_identity: None,
59 tls_identity_key: None,
60 dangerous_additional_ca_certificate: None,
61 }
62 }
63}
64
65impl GrpcClientConfig {
66 fn default_connect_timeout() -> Option<HumanDuration> {
67 Some(chrono::Duration::seconds(3).into())
68 }
69
70 pub async fn connect(&self, url: &Url) -> Result<Channel> {
72 let endpoint = Endpoint::try_from(url.to_string())?;
73 self.connect_to(endpoint).await
74 }
75
76 pub async fn connect_to(&self, mut endpoint: Endpoint) -> Result<Channel> {
78 if let Some(dur) = self.connect_timeout {
79 endpoint = endpoint.connect_timeout((*dur).to_std()?);
80 }
81 #[cfg(feature = "grpc-tls")]
82 if endpoint.uri().scheme_str() == Some("https") {
83 let ca = Certificate::from_pem(ARCHITECT_CA);
84 let mut tls_config =
85 ClientTlsConfig::new().with_enabled_roots().ca_certificate(ca);
86 if let Some(add_ca_path) = &self.dangerous_additional_ca_certificate {
87 let add_ca_pem = tokio::fs::read(add_ca_path)
88 .await
89 .with_context(|| format!("reading {}", add_ca_path.display()))?;
90 let add_ca = Certificate::from_pem(&add_ca_pem);
91 tls_config = tls_config.ca_certificate(add_ca);
92 if endpoint.uri().host().is_some_and(is_ip_address) {
94 log::error!("url is not a domain name but scheme is https; this will always fail tls domain verification");
95 log::error!("if connecting to 127.0.0.1--use \"localhost\" instead")
96 }
97 }
98 if self.tls_client || self.tls_identity.is_some() {
99 if let Some(cert_path) = &self.tls_identity {
100 let identity = grpc_tls_identity_from_pem_files(
101 cert_path,
102 self.tls_identity_key.as_ref(),
103 )
104 .await?;
105 tls_config = tls_config.identity(identity);
106 } else {
107 let mut cert_path = dirs::config_dir().ok_or_else(|| {
109 anyhow::anyhow!("could not determine default config directory")
110 })?;
111 cert_path.push("netidx");
112 cert_path.push("license.crt");
113 let identity = grpc_tls_identity_from_pem_files(
114 cert_path,
115 self.tls_identity_key.as_ref(),
116 )
117 .await?;
118 tls_config = tls_config.identity(identity);
119 }
120 }
121 endpoint = endpoint.tls_config(tls_config)?;
122 }
123 #[cfg(not(feature = "grpc-tls"))]
124 if endpoint.uri().scheme_str() == Some("https") {
125 anyhow::bail!("endpoint schema is https but grpc-tls is not enabled");
126 }
127 let channel = if self.connect_lazy {
128 endpoint.connect_lazy()
129 } else {
130 endpoint.connect().await?
131 };
132 Ok(channel)
133 }
134}
135
136#[cfg(feature = "grpc-tls")]
137fn is_ip_address(host: &str) -> bool {
138 use std::str::FromStr;
139 std::net::IpAddr::from_str(host).is_ok()
140}
141
142#[cfg(feature = "grpc-tls")]
147pub async fn grpc_tls_identity_from_pem_files(
148 cert_path: impl AsRef<std::path::Path>,
149 pkey_path: Option<impl AsRef<std::path::Path>>,
150) -> Result<tonic::transport::Identity> {
151 let cert_path = cert_path.as_ref().to_owned();
152 let pkey_path = match pkey_path {
153 Some(path) => path.as_ref().to_owned(),
154 None => cert_path.with_extension("key"),
155 };
156 let cert_pem = tokio::fs::read(&cert_path)
157 .await
158 .with_context(|| format!("reading {}", cert_path.display()))?;
159 let pkey_pem = tokio::fs::read(&pkey_path)
160 .await
161 .with_context(|| format!("reading {}", pkey_path.display()))?;
162 Ok(tonic::transport::Identity::from_pem(&cert_pem, &pkey_pem))
163}