#![forbid(unsafe_code)]
#[cfg(feature = "tls")]
mod acme;
#[cfg(feature = "tls")]
mod acme_error_map;
#[cfg(feature = "tls")]
mod client_config;
#[cfg(feature = "tls")]
pub mod commands;
#[cfg(feature = "tls")]
mod dial;
#[cfg(feature = "tls")]
mod mtls;
#[cfg(feature = "tls")]
mod paths;
#[cfg(feature = "tls")]
mod pem;
#[cfg(feature = "tls")]
mod provider;
#[cfg(feature = "tls")]
pub use acme::{
acme_complete, acme_issue_print_challenge, acme_list, acme_status, create_account,
load_account_status, AcmeDirectory,
};
#[cfg(feature = "tls")]
pub use client_config::{build_client_config, TlsClientOptions};
#[cfg(feature = "tls")]
pub use dial::{dial_tls, TlsStream};
#[cfg(feature = "tls")]
pub use mtls::{
mtls_import, mtls_list, mtls_remove, mtls_show, resolve_mtls_paths, MtlsIdentity,
};
#[cfg(feature = "tls")]
pub use paths::{
acme_account_path, acme_domain_dir, mtls_identity_dir, resolve_tls_root, tls_root_dir,
};
#[cfg(feature = "tls")]
pub use provider::{install_default_provider, provider_is_installed, provider_name};
#[derive(Debug, Clone)]
pub struct TlsConnectOptions {
pub sni: String,
pub client_cert: Option<std::path::PathBuf>,
pub client_key: Option<std::path::PathBuf>,
}
impl TlsConnectOptions {
pub fn try_new(
sni: impl Into<String>,
client_cert: Option<std::path::PathBuf>,
client_key: Option<std::path::PathBuf>,
) -> crate::errors::SshCliResult<Self> {
let sni = sni.into();
let sni_trim = sni.trim();
if sni_trim.is_empty() {
return Err(crate::errors::SshCliError::InvalidArgument(
"TLS SNI cannot be empty".into(),
));
}
match (&client_cert, &client_key) {
(Some(_), None) | (None, Some(_)) => {
return Err(crate::errors::SshCliError::InvalidArgument(
"mTLS requires both client cert and key paths".into(),
));
}
_ => {}
}
Ok(Self {
sni: sni_trim.to_owned(),
client_cert,
client_key,
})
}
}
#[cfg(not(feature = "tls"))]
pub mod disabled {
use crate::errors::SshCliResult;
pub fn install_default_provider() -> SshCliResult<()> {
Ok(())
}
#[must_use]
pub fn provider_is_installed() -> bool {
false
}
#[must_use]
pub fn provider_name() -> &'static str {
"disabled"
}
}
#[cfg(not(feature = "tls"))]
pub use disabled::{install_default_provider, provider_is_installed, provider_name};
#[cfg(all(test, feature = "tls"))]
mod tests {
use super::*;
#[test]
fn tls_options_reject_partial_mtls() {
let err = TlsConnectOptions::try_new(
"example.com",
Some(std::path::PathBuf::from("c.pem")),
None,
)
.unwrap_err();
assert!(err.to_string().contains("mTLS"));
}
#[test]
fn tls_options_reject_empty_sni() {
let err = TlsConnectOptions::try_new(" ", None, None).unwrap_err();
assert!(err.to_string().contains("SNI"));
}
#[test]
fn tls_options_ok() {
let o = TlsConnectOptions::try_new("host.example", None, None).unwrap();
assert_eq!(o.sni, "host.example");
}
}