]>
Commit | Line | Data |
---|---|---|
cb67ecad | 1 | use std::collections::HashMap; |
cb67ecad WB |
2 | use std::path::Path; |
3 | ||
4 | use anyhow::{bail, format_err, Error}; | |
d308dc8a | 5 | use serde_json::Value; |
cb67ecad | 6 | |
cb67ecad | 7 | use proxmox::sys::error::SysError; |
d308dc8a | 8 | use proxmox::tools::fs::{CreateOptions, file_read_string}; |
cb67ecad WB |
9 | |
10 | use crate::api2::types::{ | |
39c5db7f | 11 | PROXMOX_SAFE_ID_REGEX, |
d308dc8a | 12 | AcmeChallengeSchema, |
39c5db7f DM |
13 | KnownAcmeDirectory, |
14 | AcmeAccountName, | |
cb67ecad WB |
15 | }; |
16 | use crate::tools::ControlFlow; | |
17 | ||
18 | pub(crate) const ACME_DIR: &str = configdir!("/acme"); | |
19 | pub(crate) const ACME_ACCOUNT_DIR: &str = configdir!("/acme/accounts"); | |
20 | ||
d308dc8a TL |
21 | pub(crate) const ACME_DNS_SCHEMA_FN: &str = "/usr/share/proxmox-acme/dns-challenge-schema.json"; |
22 | ||
cb67ecad WB |
23 | pub mod plugin; |
24 | ||
25 | // `const fn`ify this once it is supported in `proxmox` | |
26 | fn root_only() -> CreateOptions { | |
27 | CreateOptions::new() | |
28 | .owner(nix::unistd::ROOT) | |
29 | .group(nix::unistd::Gid::from_raw(0)) | |
30 | .perm(nix::sys::stat::Mode::from_bits_truncate(0o700)) | |
31 | } | |
32 | ||
33 | fn create_acme_subdir(dir: &str) -> nix::Result<()> { | |
34 | match proxmox::tools::fs::create_dir(dir, root_only()) { | |
35 | Ok(()) => Ok(()), | |
36 | Err(err) if err.already_exists() => Ok(()), | |
37 | Err(err) => Err(err), | |
38 | } | |
39 | } | |
40 | ||
41 | pub(crate) fn make_acme_dir() -> nix::Result<()> { | |
42 | create_acme_subdir(ACME_DIR) | |
43 | } | |
44 | ||
45 | pub(crate) fn make_acme_account_dir() -> nix::Result<()> { | |
46 | make_acme_dir()?; | |
47 | create_acme_subdir(ACME_ACCOUNT_DIR) | |
48 | } | |
49 | ||
cb67ecad WB |
50 | pub const KNOWN_ACME_DIRECTORIES: &[KnownAcmeDirectory] = &[ |
51 | KnownAcmeDirectory { | |
52 | name: "Let's Encrypt V2", | |
53 | url: "https://acme-v02.api.letsencrypt.org/directory", | |
54 | }, | |
55 | KnownAcmeDirectory { | |
56 | name: "Let's Encrypt V2 Staging", | |
57 | url: "https://acme-staging-v02.api.letsencrypt.org/directory", | |
58 | }, | |
59 | ]; | |
60 | ||
61 | pub const DEFAULT_ACME_DIRECTORY_ENTRY: &KnownAcmeDirectory = &KNOWN_ACME_DIRECTORIES[0]; | |
62 | ||
63 | pub fn account_path(name: &str) -> String { | |
64 | format!("{}/{}", ACME_ACCOUNT_DIR, name) | |
65 | } | |
66 | ||
cb67ecad WB |
67 | |
68 | pub fn foreach_acme_account<F>(mut func: F) -> Result<(), Error> | |
69 | where | |
39c5db7f | 70 | F: FnMut(AcmeAccountName) -> ControlFlow<Result<(), Error>>, |
cb67ecad WB |
71 | { |
72 | match crate::tools::fs::scan_subdir(-1, ACME_ACCOUNT_DIR, &PROXMOX_SAFE_ID_REGEX) { | |
73 | Ok(files) => { | |
74 | for file in files { | |
75 | let file = file?; | |
76 | let file_name = unsafe { file.file_name_utf8_unchecked() }; | |
77 | ||
78 | if file_name.starts_with('_') { | |
79 | continue; | |
80 | } | |
81 | ||
39c5db7f DM |
82 | let account_name = match AcmeAccountName::from_string(file_name.to_owned()) { |
83 | Ok(account_name) => account_name, | |
84 | Err(_) => continue, | |
85 | }; | |
cb67ecad WB |
86 | |
87 | if let ControlFlow::Break(result) = func(account_name) { | |
88 | return result; | |
89 | } | |
90 | } | |
91 | Ok(()) | |
cb67ecad WB |
92 | } |
93 | Err(err) if err.not_found() => Ok(()), | |
94 | Err(err) => Err(err.into()), | |
95 | } | |
96 | } | |
97 | ||
98 | pub fn mark_account_deactivated(name: &str) -> Result<(), Error> { | |
99 | let from = account_path(name); | |
100 | for i in 0..100 { | |
101 | let to = account_path(&format!("_deactivated_{}_{}", name, i)); | |
102 | if !Path::new(&to).exists() { | |
103 | return std::fs::rename(&from, &to).map_err(|err| { | |
104 | format_err!( | |
105 | "failed to move account path {:?} to {:?} - {}", | |
106 | from, | |
107 | to, | |
108 | err | |
109 | ) | |
110 | }); | |
111 | } | |
112 | } | |
113 | bail!( | |
114 | "No free slot to rename deactivated account {:?}, please cleanup {:?}", | |
115 | from, | |
116 | ACME_ACCOUNT_DIR | |
117 | ); | |
118 | } | |
119 | ||
d308dc8a TL |
120 | pub fn load_dns_challenge_schema() -> Result<Vec<AcmeChallengeSchema>, Error> { |
121 | let raw = file_read_string(&ACME_DNS_SCHEMA_FN)?; | |
122 | let schemas: serde_json::Map<String, Value> = serde_json::from_str(&raw)?; | |
123 | ||
124 | Ok(schemas | |
125 | .iter() | |
126 | .map(|(id, schema)| AcmeChallengeSchema { | |
127 | id: id.to_owned(), | |
128 | name: schema | |
129 | .get("name") | |
130 | .and_then(Value::as_str) | |
131 | .unwrap_or(id) | |
132 | .to_owned(), | |
133 | ty: "dns", | |
134 | schema: schema.to_owned(), | |
135 | }) | |
136 | .collect()) | |
137 | } | |
138 | ||
cb67ecad WB |
139 | pub fn complete_acme_account(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { |
140 | let mut out = Vec::new(); | |
141 | let _ = foreach_acme_account(|name| { | |
142 | out.push(name.into_string()); | |
143 | ControlFlow::CONTINUE | |
144 | }); | |
145 | out | |
146 | } | |
147 | ||
148 | pub fn complete_acme_plugin(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
149 | match plugin::config() { | |
150 | Ok((config, _digest)) => config | |
151 | .iter() | |
152 | .map(|(id, (_type, _cfg))| id.clone()) | |
153 | .collect(), | |
154 | Err(_) => Vec::new(), | |
155 | } | |
156 | } | |
157 | ||
158 | pub fn complete_acme_plugin_type(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
e857f1fa TL |
159 | vec![ |
160 | "dns".to_string(), | |
161 | //"http".to_string(), // makes currently not realyl sense to create or the like | |
162 | ] | |
cb67ecad | 163 | } |