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::{
50 mtls_import, mtls_list, mtls_remove, mtls_show, resolve_mtls_paths, MtlsIdentity,
51};
52#[cfg(feature = "tls")]
53pub use paths::{
54 acme_account_path, acme_domain_dir, mtls_identity_dir, resolve_tls_root, tls_root_dir,
55};
56#[cfg(feature = "tls")]
57pub use provider::{install_default_provider, provider_is_installed, provider_name};
58
59#[derive(Debug, Clone)]
64pub struct TlsConnectOptions {
65 pub sni: String,
67 pub client_cert: Option<std::path::PathBuf>,
69 pub client_key: Option<std::path::PathBuf>,
71}
72
73impl TlsConnectOptions {
74 pub fn try_new(
80 sni: impl Into<String>,
81 client_cert: Option<std::path::PathBuf>,
82 client_key: Option<std::path::PathBuf>,
83 ) -> crate::errors::SshCliResult<Self> {
84 let sni = sni.into();
85 let sni_trim = sni.trim();
86 if sni_trim.is_empty() {
87 return Err(crate::errors::SshCliError::InvalidArgument(
88 "TLS SNI cannot be empty".into(),
89 ));
90 }
91 match (&client_cert, &client_key) {
92 (Some(_), None) | (None, Some(_)) => {
93 return Err(crate::errors::SshCliError::InvalidArgument(
94 "mTLS requires both client cert and key paths".into(),
95 ));
96 }
97 _ => {}
98 }
99 Ok(Self {
100 sni: sni_trim.to_owned(),
101 client_cert,
102 client_key,
103 })
104 }
105}
106
107#[cfg(not(feature = "tls"))]
109pub mod disabled {
110 use crate::errors::SshCliResult;
111
112 pub fn install_default_provider() -> SshCliResult<()> {
114 Ok(())
115 }
116
117 #[must_use]
119 pub fn provider_is_installed() -> bool {
120 false
121 }
122
123 #[must_use]
125 pub fn provider_name() -> &'static str {
126 "disabled"
127 }
128}
129
130#[cfg(not(feature = "tls"))]
131pub use disabled::{install_default_provider, provider_is_installed, provider_name};
132
133#[cfg(all(test, feature = "tls"))]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn tls_options_reject_partial_mtls() {
139 let err = TlsConnectOptions::try_new(
140 "example.com",
141 Some(std::path::PathBuf::from("c.pem")),
142 None,
143 )
144 .unwrap_err();
145 assert!(err.to_string().contains("mTLS"));
146 }
147
148 #[test]
149 fn tls_options_reject_empty_sni() {
150 let err = TlsConnectOptions::try_new(" ", None, None).unwrap_err();
151 assert!(err.to_string().contains("SNI"));
152 }
153
154 #[test]
155 fn tls_options_ok() {
156 let o = TlsConnectOptions::try_new("host.example", None, None).unwrap();
157 assert_eq!(o.sni, "host.example");
158 }
159}