use std::net::IpAddr;
use std::path::Path;
use tonic::codegen::{Body, Bytes, StdError};
use tonic::service::interceptor::InterceptedService;
use tonic::service::Interceptor;
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint};
use tonic::{Request, Status};
use crate::admin::v1::admin_service_client::AdminServiceClient;
use crate::admin::v1::git_ops_service_client::GitOpsServiceClient;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("token must not be empty")]
EmptyToken,
#[error("invalid CA PEM bundle: no certificates found")]
InvalidCaPem,
#[error("force_tls and plaintext are mutually exclusive")]
ForceTlsPlaintextConflict,
#[error("plaintext and ca_pem are mutually exclusive")]
PlaintextCaPemConflict,
#[error("invalid CA PEM bundle: {0}")]
CaPemParse(String),
#[error("invalid address {addr:?}: {source}")]
InvalidEndpoint {
addr: String,
#[source]
source: tonic::transport::Error,
},
#[error("connect to {addr:?}: {source}")]
Connect {
addr: String,
#[source]
source: tonic::transport::Error,
},
#[error("read CA file {path:?}: {source}")]
ReadCaFile {
path: String,
#[source]
source: std::io::Error,
},
#[cfg(feature = "spiffe-workload")]
#[error("connect to SPIFFE Workload API at {socket:?}: {source}")]
WorkloadApi {
socket: String,
#[source]
source: spiffe::x509_source::X509SourceError,
},
#[cfg(feature = "spiffe-workload")]
#[error("connect to SPIFFE workload API at {socket:?}: no identity issued after {attempts} attempts: {source}")]
WorkloadNoIdentityIssued {
socket: String,
attempts: usize,
#[source]
source: spiffe::WorkloadApiError,
},
#[cfg(feature = "spiffe-workload")]
#[error("connect to SPIFFE workload API at {socket:?}: {source}")]
WorkloadProbe {
socket: String,
#[source]
source: spiffe::WorkloadApiError,
},
#[cfg(feature = "spiffe-workload")]
#[error("invalid trust domain {0:?}: {1}")]
InvalidTrustDomain(String, String),
#[cfg(feature = "spiffe-workload")]
#[error("build SPIFFE mTLS client config: {0}")]
SpiffeTls(String),
#[cfg(feature = "spiffe-workload")]
#[error("invalid workload address {0:?}: expected host:port")]
InvalidWorkloadAddress(String),
#[cfg(feature = "spiffe-workload")]
#[error("connect to {addr:?}: {source}")]
WorkloadConnect {
addr: String,
#[source]
source: std::io::Error,
},
}
#[derive(Clone)]
pub struct TokenInterceptor {
header_value: tonic::metadata::MetadataValue<tonic::metadata::Ascii>,
}
impl Interceptor for TokenInterceptor {
fn call(&mut self, mut req: Request<()>) -> Result<Request<()>, Status> {
req.metadata_mut()
.insert("authorization", self.header_value.clone());
Ok(req)
}
}
pub type AdminChannel = InterceptedService<Channel, TokenInterceptor>;
pub async fn dial_admin(
addr: impl AsRef<str>,
token: impl AsRef<str>,
ca_pem: Option<&[u8]>,
force_tls: bool,
plaintext: bool,
) -> Result<AdminChannel, ClientError> {
let addr = addr.as_ref();
let token = token.as_ref().trim();
if token.is_empty() {
return Err(ClientError::EmptyToken);
}
let decision = admin_transport_decision(addr, ca_pem, force_tls, plaintext)?;
let uri = format!(
"{}://{addr}",
if decision.requires_tls() { "https" } else { "http" }
);
let mut endpoint = Endpoint::from_shared(uri).map_err(|source| ClientError::InvalidEndpoint {
addr: addr.to_string(),
source,
})?;
if let TransportDecision::Tls(tls_config) = decision {
endpoint = endpoint
.tls_config(tls_config)
.map_err(|source| ClientError::InvalidEndpoint {
addr: addr.to_string(),
source,
})?;
}
let channel = endpoint
.connect()
.await
.map_err(|source| ClientError::Connect {
addr: addr.to_string(),
source,
})?;
let header_value = format!("Bearer {token}")
.parse()
.expect("Bearer <token> is always valid ASCII metadata once token is trimmed non-empty");
Ok(InterceptedService::new(
channel,
TokenInterceptor { header_value },
))
}
pub fn admin_client<T>(channel: T) -> AdminServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
{
AdminServiceClient::new(channel)
}
pub fn gitops_client<T>(channel: T) -> GitOpsServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
{
GitOpsServiceClient::new(channel)
}
pub fn read_ca_file(path: impl AsRef<Path>) -> Result<Vec<u8>, ClientError> {
let path_ref = path.as_ref();
std::fs::read(path_ref).map_err(|source| ClientError::ReadCaFile {
path: path_ref.display().to_string(),
source,
})
}
#[derive(Debug)]
pub(crate) enum TransportDecision {
Plaintext,
Tls(ClientTlsConfig),
}
impl TransportDecision {
pub(crate) fn requires_tls(&self) -> bool {
matches!(self, TransportDecision::Tls(_))
}
}
pub(crate) fn admin_transport_decision(
addr: &str,
ca_pem: Option<&[u8]>,
force_tls: bool,
plaintext: bool,
) -> Result<TransportDecision, ClientError> {
let ca_pem_non_empty = ca_pem.map(|pem| !pem.is_empty()).unwrap_or(false);
if plaintext && force_tls {
return Err(ClientError::ForceTlsPlaintextConflict);
}
if plaintext && ca_pem_non_empty {
return Err(ClientError::PlaintextCaPemConflict);
}
if plaintext {
return Ok(TransportDecision::Plaintext);
}
let host = host_of(addr);
let use_tls = force_tls || ca_pem_non_empty || !is_loopback_host(&host);
if !use_tls {
return Ok(TransportDecision::Plaintext);
}
let mut tls = ClientTlsConfig::new();
if let Some(pem) = ca_pem {
if !pem.is_empty() {
validate_ca_pem(pem)?;
tls = tls.ca_certificate(Certificate::from_pem(pem));
}
}
Ok(TransportDecision::Tls(tls))
}
pub(crate) fn host_of(addr: &str) -> String {
if let Ok(sock) = addr.parse::<std::net::SocketAddr>() {
return sock.ip().to_string();
}
if let Some(idx) = addr.rfind(':') {
let (host_part, port_part) = (&addr[..idx], &addr[idx + 1..]);
if !host_part.is_empty() && !port_part.is_empty() && port_part.bytes().all(|b| b.is_ascii_digit()) {
return host_part.trim_start_matches('[').trim_end_matches(']').to_string();
}
}
addr.to_string()
}
pub(crate) fn is_loopback_host(host: &str) -> bool {
if host.eq_ignore_ascii_case("localhost") {
return true;
}
host.parse::<IpAddr>()
.map(|ip| ip.is_loopback())
.unwrap_or(false)
}
fn validate_ca_pem(pem: &[u8]) -> Result<(), ClientError> {
let mut reader = std::io::BufReader::new(pem);
let mut count = 0usize;
for item in rustls_pemfile::certs(&mut reader) {
match item {
Ok(_) => count += 1,
Err(e) => return Err(ClientError::CaPemParse(e.to_string())),
}
}
if count == 0 {
return Err(ClientError::InvalidCaPem);
}
Ok(())
}
#[cfg(feature = "spiffe-workload")]
mod workload {
use super::ClientError;
use std::future::Future;
use std::net::ToSocketAddrs;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::net::TcpStream;
use tonic::codegen::http::Uri;
use tonic::codegen::Service;
use tonic::transport::{Channel, Endpoint};
const WORKLOAD_DIAL_BACKOFF: [Duration; 4] = [
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(4),
Duration::from_secs(8),
];
const WORKLOAD_DIAL_MAX_ATTEMPTS: usize = WORKLOAD_DIAL_BACKOFF.len() + 1;
async fn retry_until_identity_issued<F, Fut>(
socket_path: &str,
mut probe: F,
) -> Result<(), ClientError>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<(), spiffe::WorkloadApiError>>,
{
let mut last_err: Option<spiffe::WorkloadApiError> = None;
let delays_before_each_attempt =
std::iter::once(None).chain(WORKLOAD_DIAL_BACKOFF.into_iter().map(Some));
for delay in delays_before_each_attempt {
if let Some(delay) = delay {
tokio::time::sleep(delay).await;
}
match probe().await {
Ok(()) => return Ok(()),
Err(e) => {
if !matches!(e, spiffe::WorkloadApiError::NoIdentityIssued) {
return Err(ClientError::WorkloadProbe {
socket: socket_path.to_string(),
source: e,
});
}
last_err = Some(e);
}
}
}
Err(ClientError::WorkloadNoIdentityIssued {
socket: socket_path.to_string(),
attempts: WORKLOAD_DIAL_MAX_ATTEMPTS,
source: last_err
.expect("loop always records last_err before exhausting WORKLOAD_DIAL_MAX_ATTEMPTS"),
})
}
async fn probe_identity_issued(socket_path: &str) -> Result<(), spiffe::WorkloadApiError> {
let client = spiffe::WorkloadApiClient::connect_to(socket_path).await?;
client.fetch_x509_context().await?;
Ok(())
}
pub async fn dial_workload(
addr: impl AsRef<str>,
socket_path: impl AsRef<str>,
trust_domain: impl AsRef<str>,
) -> Result<Channel, ClientError> {
let addr = addr.as_ref().to_string();
let socket_path = socket_path.as_ref().to_string();
let trust_domain = trust_domain.as_ref().to_string();
retry_until_identity_issued(&socket_path, || probe_identity_issued(&socket_path)).await?;
let source = spiffe::X509Source::builder()
.endpoint(&socket_path)
.build()
.await
.map_err(|source| ClientError::WorkloadApi {
socket: socket_path.clone(),
source,
})?;
let td = spiffe::TrustDomain::try_from(trust_domain.as_str())
.map_err(|e| ClientError::InvalidTrustDomain(trust_domain.clone(), e.to_string()))?;
let authorizer = spiffe_rustls::authorizer::trust_domains([td.clone()])
.map_err(|e| ClientError::SpiffeTls(e.to_string()))?;
let tls_config = spiffe_rustls::mtls_client(source)
.authorize(authorizer)
.trust_domain_policy(spiffe_rustls::TrustDomainPolicy::LocalOnly(td))
.with_alpn_protocols([b"h2".to_vec()])
.build()
.map_err(|e| ClientError::SpiffeTls(e.to_string()))?;
let host = super::host_of(&addr);
let server_name = rustls::pki_types::ServerName::try_from(host.clone())
.map_err(|_| ClientError::InvalidWorkloadAddress(addr.clone()))?;
let connector = SpiffeConnector {
target_addr: addr.clone(),
tls_config: Arc::new(tls_config),
server_name,
};
let endpoint =
Endpoint::from_shared(format!("http://{addr}")).map_err(|source| {
ClientError::InvalidEndpoint {
addr: addr.clone(),
source,
}
})?;
endpoint
.connect_with_connector(connector)
.await
.map_err(|source| ClientError::Connect { addr, source })
}
#[derive(Clone)]
struct SpiffeConnector {
target_addr: String,
tls_config: Arc<rustls::ClientConfig>,
server_name: rustls::pki_types::ServerName<'static>,
}
impl Service<Uri> for SpiffeConnector {
type Response = hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<TcpStream>>;
type Error = ClientError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _uri: Uri) -> Self::Future {
let target_addr = self.target_addr.clone();
let tls_config = self.tls_config.clone();
let server_name = self.server_name.clone();
Box::pin(async move {
let socket_addr = target_addr
.to_socket_addrs()
.map_err(|source| ClientError::WorkloadConnect {
addr: target_addr.clone(),
source,
})?
.next()
.ok_or_else(|| ClientError::InvalidWorkloadAddress(target_addr.clone()))?;
let tcp = TcpStream::connect(socket_addr).await.map_err(|source| {
ClientError::WorkloadConnect {
addr: target_addr.clone(),
source,
}
})?;
let _ = tcp.set_nodelay(true);
let connector = tokio_rustls::TlsConnector::from(tls_config);
let tls_stream = connector
.connect(server_name, tcp)
.await
.map_err(|source| ClientError::WorkloadConnect {
addr: target_addr,
source,
})?;
Ok(hyper_util::rt::TokioIo::new(tls_stream))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[allow(dead_code)]
fn _dial_workload_channel_satisfies_gitops_and_admin_client_bound(channel: Channel) {
let _ = super::super::gitops_client(channel.clone());
let _ = super::super::admin_client(channel);
}
fn no_identity_issued() -> spiffe::WorkloadApiError {
spiffe::WorkloadApiError::NoIdentityIssued
}
fn permission_denied(msg: &str) -> spiffe::WorkloadApiError {
spiffe::WorkloadApiError::PermissionDenied(msg.to_string())
}
#[test]
fn workload_dial_backoff_matches_verified_kluster_schedule() {
assert_eq!(
WORKLOAD_DIAL_BACKOFF,
[
Duration::from_secs(1),
Duration::from_secs(2),
Duration::from_secs(4),
Duration::from_secs(8),
]
);
assert_eq!(WORKLOAD_DIAL_MAX_ATTEMPTS, 5);
}
#[tokio::test]
async fn retry_until_identity_issued_succeeds_immediately() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_probe = calls.clone();
let result = retry_until_identity_issued("unix:///test.sock", move || {
let calls = calls_probe.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
})
.await;
assert!(result.is_ok());
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"a successful first probe must not retry"
);
}
#[tokio::test(start_paused = true)]
async fn retry_until_identity_issued_retries_no_identity_issued_then_succeeds() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_probe = calls.clone();
let start = tokio::time::Instant::now();
let result = retry_until_identity_issued("unix:///test.sock", move || {
let calls = calls_probe.clone();
async move {
let attempt = calls.fetch_add(1, Ordering::SeqCst);
if attempt < 2 {
Err(no_identity_issued())
} else {
Ok(())
}
}
})
.await;
assert!(result.is_ok());
assert_eq!(
calls.load(Ordering::SeqCst),
3,
"expected 2 failed probes then 1 succeeding probe"
);
assert_eq!(start.elapsed(), Duration::from_secs(1 + 2));
}
#[tokio::test]
async fn retry_until_identity_issued_returns_other_error_immediately_unretried() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_probe = calls.clone();
let err = retry_until_identity_issued("unix:///test.sock", move || {
let calls = calls_probe.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Err(permission_denied("selectors do not match"))
}
})
.await
.unwrap_err();
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"a non-'no identity issued' failure must never be retried"
);
match err {
ClientError::WorkloadProbe { socket, source } => {
assert_eq!(socket, "unix:///test.sock");
assert!(matches!(
source,
spiffe::WorkloadApiError::PermissionDenied(_)
));
}
other => panic!("expected ClientError::WorkloadProbe, got {other:?}"),
}
}
#[tokio::test(start_paused = true)]
async fn retry_until_identity_issued_exhausts_after_five_attempts_with_full_backoff() {
let calls = Arc::new(AtomicUsize::new(0));
let calls_probe = calls.clone();
let start = tokio::time::Instant::now();
let err = retry_until_identity_issued("unix:///test.sock", move || {
let calls = calls_probe.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Err(no_identity_issued())
}
})
.await
.unwrap_err();
assert_eq!(
calls.load(Ordering::SeqCst),
WORKLOAD_DIAL_MAX_ATTEMPTS,
"expected exactly WORKLOAD_DIAL_MAX_ATTEMPTS probes when every one fails with no identity issued"
);
assert_eq!(start.elapsed(), Duration::from_secs(1 + 2 + 4 + 8));
match err {
ClientError::WorkloadNoIdentityIssued {
socket,
attempts,
source,
} => {
assert_eq!(socket, "unix:///test.sock");
assert_eq!(attempts, WORKLOAD_DIAL_MAX_ATTEMPTS);
assert!(matches!(source, spiffe::WorkloadApiError::NoIdentityIssued));
}
other => panic!("expected ClientError::WorkloadNoIdentityIssued, got {other:?}"),
}
}
}
}
#[cfg(feature = "spiffe-workload")]
pub use workload::dial_workload;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_loopback_host_matches_go_client_table() {
let cases: &[(&str, bool)] = &[
("localhost", true),
("127.0.0.1", true),
("::1", true),
("10.0.0.5", false),
("signet.internal", false),
];
for (host, want) in cases {
assert_eq!(is_loopback_host(host), *want, "is_loopback_host({host:?})");
}
}
#[test]
fn admin_transport_decision_loopback_defaults_to_plaintext() {
let decision = admin_transport_decision("localhost:8444", None, false, false).unwrap();
assert!(!decision.requires_tls());
}
#[test]
fn admin_transport_decision_non_loopback_requires_tls() {
let decision =
admin_transport_decision("signet.internal:8444", None, false, false).unwrap();
assert!(decision.requires_tls());
}
#[test]
fn admin_transport_decision_force_tls_on_loopback() {
let decision = admin_transport_decision("localhost:8444", None, true, false).unwrap();
assert!(decision.requires_tls());
}
#[test]
fn admin_transport_decision_plaintext_overrides_non_loopback() {
let decision =
admin_transport_decision("signet.internal:8444", None, false, true).unwrap();
assert!(!decision.requires_tls());
}
#[test]
fn admin_transport_decision_plaintext_leaves_loopback_unaffected() {
let decision = admin_transport_decision("localhost:8444", None, false, true).unwrap();
assert!(!decision.requires_tls());
}
#[test]
fn admin_transport_decision_rejects_force_tls_and_plaintext_together() {
let err = admin_transport_decision("localhost:8444", None, true, true).unwrap_err();
assert!(
matches!(err, ClientError::ForceTlsPlaintextConflict),
"expected ClientError::ForceTlsPlaintextConflict, got {err:?}"
);
assert_eq!(
err.to_string(),
"force_tls and plaintext are mutually exclusive"
);
}
#[test]
fn admin_transport_decision_rejects_plaintext_with_ca_pem() {
let pem = b"-----BEGIN CERTIFICATE-----\nnot validated at this layer\n-----END CERTIFICATE-----\n";
let err = admin_transport_decision("localhost:8444", Some(pem), false, true).unwrap_err();
assert!(
matches!(err, ClientError::PlaintextCaPemConflict),
"expected ClientError::PlaintextCaPemConflict, got {err:?}"
);
assert_eq!(
err.to_string(),
"plaintext and ca_pem are mutually exclusive"
);
}
#[test]
fn admin_transport_decision_plaintext_with_empty_ca_pem_is_not_a_conflict() {
let decision = admin_transport_decision("localhost:8444", Some(&[]), false, true).unwrap();
assert!(!decision.requires_tls());
}
#[test]
fn admin_transport_decision_ca_pem_forces_tls_even_on_loopback() {
let pem = b"-----BEGIN CERTIFICATE-----\n\
MIICoDCCAYgCCQDLsJN6ayvwqTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAd0\n\
ZXN0LWNhMB4XDTI2MDcxMjIxMzM1MFoXDTI2MDcxMzIxMzM1MFowEjEQMA4GA1UE\n\
AwwHdGVzdC1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKQT/9e\n\
HkJlnufQ8dCzc0JdZRO1gHDMY6stgfZljK1dEj2SaANpP3MDVIyDcmKq/6Gwbj4K\n\
fexqB+1VGLn7CKmopYBvAIwiMDHsQ/R8xDOLwVJRCwnxzAbUUsBF9LvRkDqV4U0/\n\
i7jizdwtxDHLoB9qEkDKWo3flgIGQtgJ6Vsj7YM9CPq369fby5ZBsPCR3itvEsiZ\n\
BoM13D3A2RFywYWFpvAvzlzR6LoFd4OnH/8QMh9KTTtxNYw2K8C/a2Cv3GZRROhN\n\
g5vcQbXLSyYVBUSwdEBT50/pl97KLStN54XEE2YQvoBZCU/kUBrOP888wn+ljafk\n\
XEMVrZiAKRDnZokCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQpRqAdDsxNm+1qFf\n\
3IW8jJnfMrwdIUukE4c/ms7v3+n6QkdQYidfnZSXCrd0TAzXkRGonrFUDWAfRoGX\n\
ty0EN/hiU/wmDEvmsNgg9PS5KW3qqoIFRGYdwxn97hjJ0GdgUrbBLg0BweeaP+WW\n\
0Q7Jive55TT4W+Hwl5KETWOGi2FnvrlrDQGHWY1XKQKQn9J/tEQDMd+COyM9BHez\n\
oWg4npa5Q/5SdfJs3i4GyGRU4NWYxGfgFi7JiHOZx8t2Nv0RJkYqQu1SMNq97IDo\n\
ezQtmgLYbjPG41WWrdNT76h1mJgtlCzH0DfI7lQTBIi9AuE5poxPQiBoaC7flMsV\n\
w8cAzA==\n\
-----END CERTIFICATE-----\n";
let decision = admin_transport_decision("localhost:8444", Some(pem), false, false);
assert!(decision.is_ok());
assert!(decision.unwrap().requires_tls());
}
#[test]
fn admin_transport_decision_rejects_invalid_ca_pem() {
let err =
admin_transport_decision("signet.internal:8444", Some(b"not a cert"), false, false)
.unwrap_err();
assert!(
matches!(err, ClientError::InvalidCaPem),
"expected ClientError::InvalidCaPem, got {err:?}"
);
assert_eq!(err.to_string(), "invalid CA PEM bundle: no certificates found");
}
#[tokio::test]
async fn dial_admin_rejects_empty_token() {
let err = dial_admin("localhost:8444", " ", None, false, false)
.await
.unwrap_err();
assert!(matches!(err, ClientError::EmptyToken));
assert_eq!(err.to_string(), "token must not be empty");
}
#[tokio::test]
async fn gitops_client_and_admin_client_accept_both_channel_kinds() {
let plain_channel: Channel = Endpoint::from_static("http://localhost:1").connect_lazy();
let _gitops_over_plain_channel = gitops_client(plain_channel.clone());
let _admin_over_plain_channel = admin_client(plain_channel);
let header_value: tonic::metadata::MetadataValue<tonic::metadata::Ascii> =
"Bearer test-token".parse().unwrap();
let admin_channel: AdminChannel = InterceptedService::new(
Endpoint::from_static("http://localhost:1").connect_lazy(),
TokenInterceptor { header_value },
);
let _gitops_over_admin_channel = gitops_client(admin_channel.clone());
let _admin_over_admin_channel = admin_client(admin_channel);
}
#[test]
fn host_of_handles_bracketed_ipv6_and_bare_hosts() {
assert_eq!(host_of("localhost:8444"), "localhost");
assert_eq!(host_of("127.0.0.1:8444"), "127.0.0.1");
assert_eq!(host_of("[::1]:8444"), "::1");
assert_eq!(host_of("signet.internal"), "signet.internal");
}
}