1use std::io::BufRead;
19use std::path::PathBuf;
20
21use clap::Subcommand;
22
23use crate::cli::{CliError, resolve_profile};
24use crate::config::Config;
25use crate::signer::relay;
26
27#[derive(Subcommand)]
28pub enum UpstreamCommand {
29 Register {
32 #[arg(long = "eab-kid")]
35 eab_kid: Option<String>,
36 #[arg(long = "eab-hmac-key-file")]
39 eab_hmac_key_file: Option<PathBuf>,
40 #[arg(long)]
43 profile: Option<String>,
44 },
45 Show {
47 #[arg(long)]
48 json: bool,
49 #[arg(long)]
52 profile: Option<String>,
53 },
54}
55
56pub async fn run_upstream_command(
65 command: UpstreamCommand,
66 reader: &mut impl BufRead,
67 config: &Config,
68) -> Result<(), CliError> {
69 match command {
70 UpstreamCommand::Register {
71 eab_kid,
72 eab_hmac_key_file,
73 profile,
74 } => {
75 let resolved = resolve_profile(config, profile.as_deref())?;
76 let cfg = &resolved.sections.signer.relay;
77 if cfg.directory_url.is_empty() {
78 return Err(CliError(
79 "signer.relay.directory_url is not set: there is no upstream to register \
80 with"
81 .to_string(),
82 ));
83 }
84
85 let secret = eab_kid
88 .as_ref()
89 .map(|_| read_secret(eab_hmac_key_file.as_deref(), reader))
90 .transpose()?;
91 let eab = eab_kid.as_deref().zip(secret.as_deref());
92
93 let resolver = crate::dns::resolver_addr(&config.dns)
97 .and_then(crate::challenge::build_resolver)
98 .map_err(|error| CliError(format!("configuration error: {error}")))?;
99 let proxies = crate::proxy::from_config(&config.proxy)
100 .map_err(|error| CliError(format!("configuration error: {error}")))?;
101 let outbound = crate::http_client::Outbound::new(resolver, proxies);
102 match relay::register_upstream_account(cfg, outbound, eab).await {
103 Ok(kid) => println!("Registered. kid = {kid}"),
104 Err(error) => {
105 return Err(CliError(format!("upstream registration failed: {error}")));
106 }
107 }
108 }
109
110 UpstreamCommand::Show { json, profile } => {
111 let resolved = resolve_profile(config, profile.as_deref())?;
112 let cfg = &resolved.sections.signer.relay;
113 let kid = relay::stored_kid(cfg);
114 let key_present = std::path::Path::new(&cfg.account_key_path).exists();
115
116 if json {
117 println!(
118 "{}",
119 serde_json::json!({
120 "directoryUrl": cfg.directory_url,
121 "accountKeyPath": cfg.account_key_path,
122 "accountKeyPresent": key_present,
123 "kid": kid,
124 "registered": kid.is_some(),
125 })
126 );
127 } else {
128 println!("directory: {}", none_if_empty(&cfg.directory_url));
129 println!(
130 "account key: {} ({})",
131 cfg.account_key_path,
132 if key_present { "present" } else { "absent" }
133 );
134 match kid {
135 Some(kid) => println!("kid: {kid}"),
136 None => println!("kid: (not registered)"),
137 }
138 }
139 }
140 }
141 Ok(())
142}
143
144fn none_if_empty(value: &str) -> &str {
145 if value.is_empty() { "(not set)" } else { value }
146}
147
148fn read_secret(
156 path: Option<&std::path::Path>,
157 reader: &mut impl BufRead,
158) -> Result<Vec<u8>, CliError> {
159 let raw = match path {
160 Some(path) => std::fs::read_to_string(path)
161 .map_err(|error| CliError(format!("cannot read {}: {error}", path.display())))?,
162 None => {
163 eprintln!("Enter the upstream EAB HMAC key (base64), then press Enter:");
164 let mut line = String::new();
165 if reader.read_line(&mut line).unwrap_or(0) == 0 {
166 return Err(CliError("no EAB key supplied".to_string()));
167 }
168 line
169 }
170 };
171
172 relay::decode_secret(raw.trim())
173 .ok_or_else(|| CliError("the EAB key is not valid base64".to_string()))
174}
175
176#[cfg(test)]
177mod tests {
178 use crate::config::ENV_LOCK;
179
180 fn config_from(body: &str) -> Config {
183 let _lock = ENV_LOCK
184 .lock()
185 .unwrap_or_else(std::sync::PoisonError::into_inner);
186 let dir = crate::testutil::TempDir::new("upstream");
187 std::fs::write(dir.join("config.toml"), body).unwrap();
188 unsafe {
190 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
191 }
192 let config = Config::load().expect("the configuration must load");
193 unsafe {
194 std::env::remove_var("ACME_PROXY_CONFIG");
195 }
196 config
197 }
198
199 #[test]
204 fn a_profiles_own_upstream_is_what_gets_resolved() {
205 let config = config_from(
206 r#"
207 [profiles.le]
208 signer.backend = "relay"
209 signer.relay.directory_url = "https://upstream.example/directory"
210 signer.relay.account_key_path = "/tmp/le-upstream.key"
211 "#,
212 );
213
214 let resolved = resolve_profile(&config, None).unwrap();
215 assert_eq!(resolved.name, "le");
216 assert_eq!(
217 resolved.sections.signer.relay.directory_url,
218 "https://upstream.example/directory"
219 );
220 assert_eq!(
221 resolved.sections.signer.relay.account_key_path,
222 "/tmp/le-upstream.key"
223 );
224 }
225
226 #[test]
230 fn several_profiles_require_saying_which() {
231 let config = config_from(
232 r#"
233 [profiles.le]
234 [profiles.internal]
235 "#,
236 );
237
238 let error = resolve_profile(&config, None).unwrap_err().to_string();
239 assert!(error.contains("--profile"), "{error}");
240 assert!(
241 error.contains("le") && error.contains("internal"),
242 "{error}"
243 );
244
245 assert_eq!(resolve_profile(&config, Some("le")).unwrap().name, "le");
247 let error = resolve_profile(&config, Some("nope"))
249 .unwrap_err()
250 .to_string();
251 assert!(error.contains("nope"), "{error}");
252 }
253
254 use super::*;
255 use base64::prelude::*;
256
257 #[test]
262 fn an_empty_directory_url_renders_as_unset() {
263 assert_eq!(none_if_empty(""), "(not set)");
264 assert_eq!(none_if_empty("https://x/dir"), "https://x/dir");
265 }
266
267 #[test]
269 fn the_secret_can_come_from_stdin() {
270 let secret = b"01234567890123456789012345678901";
271 let encoded = BASE64_URL_SAFE_NO_PAD.encode(secret);
272 let mut reader = std::io::Cursor::new(format!("{encoded}\n").into_bytes());
273 assert_eq!(read_secret(None, &mut reader).unwrap(), secret.to_vec());
274 }
275
276 #[test]
277 fn the_secret_can_come_from_a_file() {
278 let secret = b"01234567890123456789012345678901";
279 let dir = crate::testutil::TempDir::new("eab");
280 let path = dir.join("key.b64");
281 std::fs::write(
283 &path,
284 format!("{}\n", BASE64_URL_SAFE_NO_PAD.encode(secret)),
285 )
286 .unwrap();
287
288 let mut empty = std::io::Cursor::new(Vec::new());
289 assert_eq!(
290 read_secret(Some(&path), &mut empty).unwrap(),
291 secret.to_vec()
292 );
293 }
294
295 #[test]
296 fn a_missing_secret_file_is_reported() {
297 let mut empty = std::io::Cursor::new(Vec::new());
298 let error = read_secret(
299 Some(std::path::Path::new("/nonexistent/eab.b64")),
300 &mut empty,
301 )
302 .expect_err("a missing file must be reported");
303 assert!(error.to_string().starts_with("cannot read "), "{error}");
304 }
305
306 #[test]
309 fn an_empty_stdin_is_reported() {
310 let mut empty = std::io::Cursor::new(Vec::new());
311 assert_eq!(
312 read_secret(None, &mut empty),
313 Err(CliError("no EAB key supplied".to_string()))
314 );
315 }
316
317 #[test]
318 fn a_secret_that_is_not_base64_is_reported() {
319 let mut reader = std::io::Cursor::new(b"not base64!!!\n".to_vec());
320 assert_eq!(
321 read_secret(None, &mut reader),
322 Err(CliError("the EAB key is not valid base64".to_string()))
323 );
324 }
325
326 #[tokio::test]
329 async fn registering_without_an_upstream_is_refused() {
330 let config = config_from("[profiles.default]\n");
335 let mut reader: &[u8] = &[];
336 let error = run_upstream_command(
337 UpstreamCommand::Register {
338 eab_kid: None,
339 eab_hmac_key_file: None,
340 profile: None,
341 },
342 &mut reader,
343 &config,
344 )
345 .await
346 .expect_err("there is no upstream to register with");
347 assert!(error.to_string().contains("directory_url"), "{error}");
348 }
349
350 #[tokio::test]
353 async fn an_unreachable_upstream_is_reported() {
354 let dir = crate::testutil::TempDir::new("upstream");
355
356 let config = config_from(&format!(
359 r#"
360 [profiles.default]
361 signer.backend = "relay"
362 signer.relay.directory_url = "http://127.0.0.1:1/directory"
363 signer.relay.account_key_path = "{}"
364 "#,
365 dir.join("upstream.key").display()
366 ));
367
368 let secret = BASE64_URL_SAFE_NO_PAD.encode(b"01234567890123456789012345678901");
371 let mut reader = std::io::Cursor::new(format!("{secret}\n").into_bytes());
372 let error = run_upstream_command(
373 UpstreamCommand::Register {
374 eab_kid: Some("kid-1".to_string()),
375 eab_hmac_key_file: None,
376 profile: None,
377 },
378 &mut reader,
379 &config,
380 )
381 .await
382 .expect_err("nothing is listening on that port");
383 assert!(
384 error
385 .to_string()
386 .starts_with("upstream registration failed: "),
387 "{error}"
388 );
389 }
390
391 #[tokio::test]
394 async fn show_renders_an_unregistered_upstream() {
395 let config = config_from("[profiles.default]\n");
396 for json in [true, false] {
397 let mut reader: &[u8] = &[];
398 run_upstream_command(
399 UpstreamCommand::Show {
400 json,
401 profile: None,
402 },
403 &mut reader,
404 &config,
405 )
406 .await
407 .unwrap();
408 }
409 }
410}