1#![forbid(unsafe_code)]
4#[cfg(feature = "tls")]
21mod acme;
22#[cfg(feature = "tls")]
23mod acme_error_map;
24#[cfg(feature = "tls")]
25mod client_config;
26#[cfg(feature = "tls")]
27pub mod commands;
28#[cfg(feature = "tls")]
29mod dial;
30#[cfg(feature = "tls")]
31mod mtls;
32#[cfg(feature = "tls")]
33mod paths;
34#[cfg(feature = "tls")]
35mod pem;
36#[cfg(feature = "tls")]
37mod provider;
38
39#[cfg(feature = "tls")]
40pub use acme::{
41 acme_complete, acme_issue_print_challenge, acme_list, acme_status, create_account,
42 load_account_status, AcmeDirectory,
43};
44#[cfg(feature = "tls")]
45pub use client_config::{build_client_config, TlsClientOptions};
46#[cfg(feature = "tls")]
47pub use dial::{dial_tls, TlsStream};
48#[cfg(feature = "tls")]
49pub use mtls::{mtls_import, mtls_list, mtls_remove, mtls_show, resolve_mtls_paths, MtlsIdentity};
50#[cfg(feature = "tls")]
51pub use paths::{
52 acme_account_path, acme_domain_dir, mtls_identity_dir, resolve_tls_root, tls_root_dir,
53};
54#[cfg(feature = "tls")]
55pub use provider::{install_default_provider, provider_is_installed, provider_name};
56
57#[derive(Debug, Clone)]
62pub struct TlsConnectOptions {
63 pub sni: String,
65 pub client_cert: Option<std::path::PathBuf>,
67 pub client_key: Option<std::path::PathBuf>,
69}
70
71impl TlsConnectOptions {
72 pub fn try_new(
78 sni: impl Into<String>,
79 client_cert: Option<std::path::PathBuf>,
80 client_key: Option<std::path::PathBuf>,
81 ) -> crate::errors::SshCliResult<Self> {
82 let sni = sni.into();
83 let sni_trim = sni.trim();
84 if sni_trim.is_empty() {
85 return Err(crate::errors::SshCliError::InvalidArgument(
86 "TLS SNI cannot be empty".into(),
87 ));
88 }
89 match (&client_cert, &client_key) {
90 (Some(_), None) | (None, Some(_)) => {
91 return Err(crate::errors::SshCliError::InvalidArgument(
92 "mTLS requires both client cert and key paths".into(),
93 ));
94 }
95 _ => {}
96 }
97 Ok(Self {
98 sni: sni_trim.to_owned(),
99 client_cert,
100 client_key,
101 })
102 }
103}
104
105#[cfg(not(feature = "tls"))]
107pub mod disabled {
108 use crate::errors::SshCliResult;
109
110 pub fn install_default_provider() -> SshCliResult<()> {
112 Ok(())
113 }
114
115 #[must_use]
117 pub fn provider_is_installed() -> bool {
118 false
119 }
120
121 #[must_use]
123 pub fn provider_name() -> &'static str {
124 "disabled"
125 }
126}
127
128#[cfg(not(feature = "tls"))]
129pub use disabled::{install_default_provider, provider_is_installed, provider_name};
130
131#[cfg(all(test, feature = "tls"))]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn tls_options_reject_partial_mtls() {
137 let err = TlsConnectOptions::try_new(
138 "example.com",
139 Some(std::path::PathBuf::from("c.pem")),
140 None,
141 )
142 .unwrap_err();
143 assert!(err.to_string().contains("mTLS"));
144 }
145
146 #[test]
147 fn tls_options_reject_empty_sni() {
148 let err = TlsConnectOptions::try_new(" ", None, None).unwrap_err();
149 assert!(err.to_string().contains("SNI"));
150 }
151
152 #[test]
153 fn tls_options_ok() {
154 let o = TlsConnectOptions::try_new("host.example", None, None).unwrap();
155 assert_eq!(o.sni, "host.example");
156 }
157}