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