1#![forbid(unsafe_code)]
3use std::path::PathBuf;
6
7use crate::cli::{OutputFormat, TlsAcmeAccountAction, TlsAcmeAction, TlsAction, TlsMtlsAction};
8use crate::errors::SshCliError;
9use crate::output;
10use anyhow::Result;
11
12pub async fn run_tls_command(
14 action: TlsAction,
15 config_override: Option<PathBuf>,
16 format: OutputFormat,
17 json_flag: bool,
18) -> Result<()> {
19 let json = format == OutputFormat::Json || json_flag;
20 let cfg = config_override.as_deref();
21
22 match action {
23 TlsAction::Provider => {
24 let installed = super::provider_is_installed();
25 let name = super::provider_name();
26 let payload = serde_json::json!({
27 "provider": name,
28 "installed": installed,
29 "min_rustls": "0.23.18",
30 "stack": "rustls+aws_lc_rs",
31 });
32 output::emit_success(
33 "tls-provider",
34 payload,
35 &format!("TLS provider: {name} (installed={installed})"),
36 json,
37 )?;
38 }
39 TlsAction::Paths => {
40 let root = super::tls_root_dir(cfg)?;
41 let account = super::acme_account_path(cfg)?;
42 let payload = serde_json::json!({
43 "tls_root": root.display().to_string(),
44 "mtls_dir": root.join(crate::constants::TLS_MTLS_DIR_NAME).display().to_string(),
45 "acme_dir": root.join(crate::constants::TLS_ACME_DIR_NAME).display().to_string(),
46 "acme_account": account.display().to_string(),
47 });
48 output::emit_success(
49 "tls-paths",
50 payload,
51 &format!("TLS root: {}", root.display()),
52 json,
53 )?;
54 }
55 TlsAction::Mtls { action } => match action {
56 TlsMtlsAction::List => {
57 let names = super::mtls_list(cfg)?;
58 let payload = serde_json::json!({ "identities": names });
59 let msg = if names.is_empty() {
60 "no mTLS identities".to_string()
61 } else {
62 format!("mTLS identities: {}", names.join(", "))
63 };
64 output::emit_success("tls-mtls-list", payload, &msg, json)?;
65 }
66 TlsMtlsAction::Import { name, cert, key } => {
67 let id = super::mtls_import(cfg, &name, &cert, &key)?;
68 let payload = serde_json::json!({
69 "name": id.name,
70 "cert_path": id.cert_path.display().to_string(),
71 "key_path": id.key_path.display().to_string(),
72 });
73 output::emit_success(
74 "tls-mtls-import",
75 payload,
76 &format!("imported mTLS identity '{}'", id.name),
77 json,
78 )?;
79 }
80 TlsMtlsAction::Show { name } => {
81 let id = super::mtls_show(cfg, &name)?;
82 let payload = serde_json::json!({
83 "name": id.name,
84 "cert_path": id.cert_path.display().to_string(),
85 "key_path": id.key_path.display().to_string(),
86 });
87 output::emit_success(
88 "tls-mtls-show",
89 payload,
90 &format!(
91 "mTLS '{}': cert={} key={}",
92 id.name,
93 id.cert_path.display(),
94 id.key_path.display()
95 ),
96 json,
97 )?;
98 }
99 TlsMtlsAction::Remove { name } => {
100 super::mtls_remove(cfg, &name)?;
101 output::emit_success(
102 "tls-mtls-remove",
103 serde_json::json!({ "name": name }),
104 &format!("removed mTLS identity '{name}'"),
105 json,
106 )?;
107 }
108 },
109 TlsAction::Acme { action } => match action {
110 TlsAcmeAction::Account { action } => match action {
111 TlsAcmeAccountAction::Create {
112 staging,
113 contact,
114 force,
115 } => {
116 let dir = if staging {
117 super::AcmeDirectory::Staging
118 } else {
119 super::AcmeDirectory::Production
120 };
121 let st = super::create_account(cfg, dir, &contact, force).await?;
122 let payload = serde_json::to_value(&st)
123 .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
124 output::emit_success(
125 "tls-acme-account-create",
126 payload,
127 &format!("ACME account at {}", st.path),
128 json,
129 )?;
130 }
131 TlsAcmeAccountAction::Show => {
132 let st = super::load_account_status(cfg)?;
133 let payload = serde_json::to_value(&st)
134 .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
135 let msg = if st.present {
136 format!("ACME account present at {}", st.path)
137 } else {
138 format!("no ACME account at {}", st.path)
139 };
140 output::emit_success("tls-acme-account-show", payload, &msg, json)?;
141 }
142 },
143 TlsAcmeAction::Issue {
144 domain,
145 staging,
146 print_challenge,
147 } => {
148 if !print_challenge {
149 return Err(SshCliError::InvalidArgument(
150 "pass --print-challenge (agent two-step DNS-01; no interactive wait)"
151 .into(),
152 )
153 .into());
154 }
155 let dir = if staging {
156 super::AcmeDirectory::Staging
157 } else {
158 super::AcmeDirectory::Production
159 };
160 let pending = super::acme_issue_print_challenge(cfg, &domain, dir).await?;
161 let payload = serde_json::to_value(&pending)
162 .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
163 let msg = format!(
164 "set DNS TXT {} = {} then run: ssh-cli tls acme complete --domain {}",
165 pending.dns_name, pending.dns_value, pending.domain
166 );
167 output::emit_success("tls-acme-challenge", payload, &msg, json)?;
168 }
169 TlsAcmeAction::Complete { domain } => {
170 let st = super::acme_complete(cfg, &domain).await?;
171 let payload = serde_json::to_value(&st)
172 .map_err(|e| SshCliError::tls_msg(format!("json: {e}")))?;
173 output::emit_success(
174 "tls-acme-complete",
175 payload,
176 &format!("certificate issued for {domain}"),
177 json,
178 )?;
179 }
180 TlsAcmeAction::Status { domain } => {
181 let list = super::acme_status(cfg, domain.as_deref())?;
182 let payload = serde_json::json!({ "domains": list });
183 output::emit_success(
184 "tls-acme-status",
185 payload,
186 &format!("{} ACME domain(s)", list.len()),
187 json,
188 )?;
189 }
190 TlsAcmeAction::List => {
191 let list = super::acme_list(cfg)?;
192 let payload = serde_json::json!({ "domains": list });
193 output::emit_success(
194 "tls-acme-list",
195 payload,
196 &format!("{} ACME domain(s)", list.len()),
197 json,
198 )?;
199 }
200 },
201 }
202 Ok(())
203}