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