]>
Commit | Line | Data |
---|---|---|
cb67ecad WB |
1 | use anyhow::Error; |
2 | use lazy_static::lazy_static; | |
3 | use serde::{Deserialize, Serialize}; | |
4 | use serde_json::Value; | |
5 | ||
6 | use proxmox::api::{ | |
7 | api, | |
8 | schema::*, | |
9 | section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin}, | |
10 | }; | |
11 | ||
12 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; | |
13 | ||
14 | use crate::api2::types::PROXMOX_SAFE_ID_FORMAT; | |
15 | ||
16 | pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.") | |
17 | .format(&PROXMOX_SAFE_ID_FORMAT) | |
18 | .schema(); | |
19 | ||
20 | lazy_static! { | |
21 | pub static ref CONFIG: SectionConfig = init(); | |
22 | } | |
23 | ||
24 | #[api( | |
25 | properties: { | |
26 | id: { schema: PLUGIN_ID_SCHEMA }, | |
27 | }, | |
28 | )] | |
29 | #[derive(Deserialize, Serialize)] | |
30 | /// Standalone ACME Plugin for the http-1 challenge. | |
31 | pub struct StandalonePlugin { | |
32 | /// Plugin ID. | |
33 | id: String, | |
34 | } | |
35 | ||
36 | impl Default for StandalonePlugin { | |
37 | fn default() -> Self { | |
38 | Self { | |
39 | id: "standalone".to_string(), | |
40 | } | |
41 | } | |
42 | } | |
43 | ||
44 | #[api( | |
45 | properties: { | |
46 | id: { schema: PLUGIN_ID_SCHEMA }, | |
47 | disable: { | |
48 | optional: true, | |
49 | default: false, | |
50 | }, | |
51 | "validation-delay": { | |
52 | default: 30, | |
53 | optional: true, | |
54 | minimum: 0, | |
55 | maximum: 2 * 24 * 60 * 60, | |
56 | }, | |
57 | }, | |
58 | )] | |
59 | /// DNS ACME Challenge Plugin core data. | |
60 | #[derive(Deserialize, Serialize, Updater)] | |
61 | #[serde(rename_all = "kebab-case")] | |
62 | pub struct DnsPluginCore { | |
63 | /// Plugin ID. | |
64 | pub(crate) id: String, | |
65 | ||
66 | /// DNS API Plugin Id. | |
67 | pub(crate) api: String, | |
68 | ||
69 | /// Extra delay in seconds to wait before requesting validation. | |
70 | /// | |
71 | /// Allows to cope with long TTL of DNS records. | |
72 | #[serde(skip_serializing_if = "Option::is_none", default)] | |
73 | validation_delay: Option<u32>, | |
74 | ||
75 | /// Flag to disable the config. | |
76 | #[serde(skip_serializing_if = "Option::is_none", default)] | |
77 | disable: Option<bool>, | |
78 | } | |
79 | ||
80 | #[api( | |
81 | properties: { | |
82 | core: { type: DnsPluginCore }, | |
83 | }, | |
84 | )] | |
85 | /// DNS ACME Challenge Plugin. | |
86 | #[derive(Deserialize, Serialize)] | |
87 | #[serde(rename_all = "kebab-case")] | |
88 | pub struct DnsPlugin { | |
89 | #[serde(flatten)] | |
90 | pub(crate) core: DnsPluginCore, | |
91 | ||
92 | // FIXME: The `Updater` should allow: | |
93 | // * having different descriptions for this and the Updater version | |
94 | // * having different `#[serde]` attributes for the Updater | |
95 | // * or, well, leaving fields out completely in teh Updater but this means we may need to | |
96 | // separate Updater and Builder deriving. | |
97 | // We handle this property separately in the API calls. | |
98 | /// DNS plugin data (base64url encoded without padding). | |
99 | #[serde(with = "proxmox::tools::serde::string_as_base64url_nopad")] | |
100 | pub(crate) data: String, | |
101 | } | |
102 | ||
103 | impl DnsPlugin { | |
104 | pub fn decode_data(&self, output: &mut Vec<u8>) -> Result<(), Error> { | |
105 | Ok(base64::decode_config_buf( | |
106 | &self.data, | |
107 | base64::URL_SAFE_NO_PAD, | |
108 | output, | |
109 | )?) | |
110 | } | |
111 | } | |
112 | ||
113 | fn init() -> SectionConfig { | |
114 | let mut config = SectionConfig::new(&PLUGIN_ID_SCHEMA); | |
115 | ||
116 | let standalone_schema = match &StandalonePlugin::API_SCHEMA { | |
117 | Schema::Object(schema) => schema, | |
118 | _ => unreachable!(), | |
119 | }; | |
120 | let standalone_plugin = SectionConfigPlugin::new( | |
121 | "standalone".to_string(), | |
122 | Some("id".to_string()), | |
123 | standalone_schema, | |
124 | ); | |
125 | config.register_plugin(standalone_plugin); | |
126 | ||
127 | let dns_challenge_schema = match DnsPlugin::API_SCHEMA { | |
128 | Schema::AllOf(ref schema) => schema, | |
129 | _ => unreachable!(), | |
130 | }; | |
131 | let dns_challenge_plugin = SectionConfigPlugin::new( | |
132 | "dns".to_string(), | |
133 | Some("id".to_string()), | |
134 | dns_challenge_schema, | |
135 | ); | |
136 | config.register_plugin(dns_challenge_plugin); | |
137 | ||
138 | config | |
139 | } | |
140 | ||
141 | const ACME_PLUGIN_CFG_FILENAME: &str = configdir!("/acme/plugins.cfg"); | |
142 | const ACME_PLUGIN_CFG_LOCKFILE: &str = configdir!("/acme/.plugins.lck"); | |
143 | const LOCK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); | |
144 | ||
145 | pub fn lock() -> Result<std::fs::File, Error> { | |
146 | super::make_acme_dir()?; | |
147 | proxmox::tools::fs::open_file_locked(ACME_PLUGIN_CFG_LOCKFILE, LOCK_TIMEOUT, true) | |
148 | } | |
149 | ||
150 | pub fn config() -> Result<(PluginData, [u8; 32]), Error> { | |
151 | let content = proxmox::tools::fs::file_read_optional_string(ACME_PLUGIN_CFG_FILENAME)? | |
152 | .unwrap_or_else(|| "".to_string()); | |
153 | ||
154 | let digest = openssl::sha::sha256(content.as_bytes()); | |
155 | let mut data = CONFIG.parse(ACME_PLUGIN_CFG_FILENAME, &content)?; | |
156 | ||
157 | if data.sections.get("standalone").is_none() { | |
158 | let standalone = StandalonePlugin::default(); | |
159 | data.set_data("standalone", "standalone", &standalone) | |
160 | .unwrap(); | |
161 | } | |
162 | ||
163 | Ok((PluginData { data }, digest)) | |
164 | } | |
165 | ||
166 | pub fn save_config(config: &PluginData) -> Result<(), Error> { | |
167 | super::make_acme_dir()?; | |
168 | let raw = CONFIG.write(ACME_PLUGIN_CFG_FILENAME, &config.data)?; | |
169 | ||
170 | let backup_user = crate::backup::backup_user()?; | |
171 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); | |
172 | // set the correct owner/group/permissions while saving file | |
173 | // owner(rw) = root, group(r)= backup | |
174 | let options = CreateOptions::new() | |
175 | .perm(mode) | |
176 | .owner(nix::unistd::ROOT) | |
177 | .group(backup_user.gid); | |
178 | ||
179 | replace_file(ACME_PLUGIN_CFG_FILENAME, raw.as_bytes(), options)?; | |
180 | ||
181 | Ok(()) | |
182 | } | |
183 | ||
184 | pub struct PluginData { | |
185 | data: SectionConfigData, | |
186 | } | |
187 | ||
188 | // And some convenience helpers. | |
189 | impl PluginData { | |
190 | pub fn remove(&mut self, name: &str) -> Option<(String, Value)> { | |
191 | self.data.sections.remove(name) | |
192 | } | |
193 | ||
194 | pub fn contains_key(&mut self, name: &str) -> bool { | |
195 | self.data.sections.contains_key(name) | |
196 | } | |
197 | ||
198 | pub fn get(&self, name: &str) -> Option<&(String, Value)> { | |
199 | self.data.sections.get(name) | |
200 | } | |
201 | ||
202 | pub fn get_mut(&mut self, name: &str) -> Option<&mut (String, Value)> { | |
203 | self.data.sections.get_mut(name) | |
204 | } | |
205 | ||
206 | pub fn insert(&mut self, id: String, ty: String, plugin: Value) { | |
207 | self.data.sections.insert(id, (ty, plugin)); | |
208 | } | |
209 | ||
210 | pub fn iter(&self) -> impl Iterator<Item = (&String, &(String, Value))> + Send { | |
211 | self.data.sections.iter() | |
212 | } | |
213 | } |