]>
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 | ||
cb67ecad | 12 | use crate::api2::types::PROXMOX_SAFE_ID_FORMAT; |
7526d864 | 13 | use crate::backup::{open_backup_lockfile, BackupLockGuard}; |
cb67ecad WB |
14 | |
15 | pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.") | |
16 | .format(&PROXMOX_SAFE_ID_FORMAT) | |
39c5db7f DM |
17 | .min_length(1) |
18 | .max_length(32) | |
cb67ecad WB |
19 | .schema(); |
20 | ||
21 | lazy_static! { | |
22 | pub static ref CONFIG: SectionConfig = init(); | |
23 | } | |
24 | ||
25 | #[api( | |
26 | properties: { | |
27 | id: { schema: PLUGIN_ID_SCHEMA }, | |
28 | }, | |
29 | )] | |
30 | #[derive(Deserialize, Serialize)] | |
31 | /// Standalone ACME Plugin for the http-1 challenge. | |
32 | pub struct StandalonePlugin { | |
33 | /// Plugin ID. | |
34 | id: String, | |
35 | } | |
36 | ||
37 | impl Default for StandalonePlugin { | |
38 | fn default() -> Self { | |
39 | Self { | |
40 | id: "standalone".to_string(), | |
41 | } | |
42 | } | |
43 | } | |
44 | ||
45 | #[api( | |
46 | properties: { | |
47 | id: { schema: PLUGIN_ID_SCHEMA }, | |
48 | disable: { | |
49 | optional: true, | |
50 | default: false, | |
51 | }, | |
52 | "validation-delay": { | |
53 | default: 30, | |
54 | optional: true, | |
55 | minimum: 0, | |
56 | maximum: 2 * 24 * 60 * 60, | |
57 | }, | |
58 | }, | |
59 | )] | |
60 | /// DNS ACME Challenge Plugin core data. | |
61 | #[derive(Deserialize, Serialize, Updater)] | |
62 | #[serde(rename_all = "kebab-case")] | |
63 | pub struct DnsPluginCore { | |
64 | /// Plugin ID. | |
a8a20e92 DM |
65 | #[updater(skip)] |
66 | pub id: String, | |
cb67ecad WB |
67 | |
68 | /// DNS API Plugin Id. | |
a8a20e92 | 69 | pub api: String, |
cb67ecad WB |
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)] | |
a8a20e92 | 75 | pub validation_delay: Option<u32>, |
cb67ecad WB |
76 | |
77 | /// Flag to disable the config. | |
78 | #[serde(skip_serializing_if = "Option::is_none", default)] | |
a8a20e92 | 79 | pub disable: Option<bool>, |
cb67ecad WB |
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)] | |
a8a20e92 | 92 | pub core: DnsPluginCore, |
cb67ecad | 93 | |
cb67ecad WB |
94 | // We handle this property separately in the API calls. |
95 | /// DNS plugin data (base64url encoded without padding). | |
96 | #[serde(with = "proxmox::tools::serde::string_as_base64url_nopad")] | |
a8a20e92 | 97 | pub data: String, |
cb67ecad WB |
98 | } |
99 | ||
100 | impl DnsPlugin { | |
101 | pub fn decode_data(&self, output: &mut Vec<u8>) -> Result<(), Error> { | |
102 | Ok(base64::decode_config_buf( | |
103 | &self.data, | |
104 | base64::URL_SAFE_NO_PAD, | |
105 | output, | |
106 | )?) | |
107 | } | |
108 | } | |
109 | ||
110 | fn init() -> SectionConfig { | |
111 | let mut config = SectionConfig::new(&PLUGIN_ID_SCHEMA); | |
112 | ||
113 | let standalone_schema = match &StandalonePlugin::API_SCHEMA { | |
114 | Schema::Object(schema) => schema, | |
115 | _ => unreachable!(), | |
116 | }; | |
117 | let standalone_plugin = SectionConfigPlugin::new( | |
118 | "standalone".to_string(), | |
119 | Some("id".to_string()), | |
120 | standalone_schema, | |
121 | ); | |
122 | config.register_plugin(standalone_plugin); | |
123 | ||
124 | let dns_challenge_schema = match DnsPlugin::API_SCHEMA { | |
125 | Schema::AllOf(ref schema) => schema, | |
126 | _ => unreachable!(), | |
127 | }; | |
128 | let dns_challenge_plugin = SectionConfigPlugin::new( | |
129 | "dns".to_string(), | |
130 | Some("id".to_string()), | |
131 | dns_challenge_schema, | |
132 | ); | |
133 | config.register_plugin(dns_challenge_plugin); | |
134 | ||
135 | config | |
136 | } | |
137 | ||
af06decd WB |
138 | const ACME_PLUGIN_CFG_FILENAME: &str = pbs_buildcfg::configdir!("/acme/plugins.cfg"); |
139 | const ACME_PLUGIN_CFG_LOCKFILE: &str = pbs_buildcfg::configdir!("/acme/.plugins.lck"); | |
cb67ecad | 140 | |
7526d864 | 141 | pub fn lock() -> Result<BackupLockGuard, Error> { |
cb67ecad | 142 | super::make_acme_dir()?; |
7526d864 | 143 | open_backup_lockfile(ACME_PLUGIN_CFG_LOCKFILE, None, true) |
cb67ecad WB |
144 | } |
145 | ||
146 | pub fn config() -> Result<(PluginData, [u8; 32]), Error> { | |
147 | let content = proxmox::tools::fs::file_read_optional_string(ACME_PLUGIN_CFG_FILENAME)? | |
148 | .unwrap_or_else(|| "".to_string()); | |
149 | ||
150 | let digest = openssl::sha::sha256(content.as_bytes()); | |
151 | let mut data = CONFIG.parse(ACME_PLUGIN_CFG_FILENAME, &content)?; | |
152 | ||
153 | if data.sections.get("standalone").is_none() { | |
154 | let standalone = StandalonePlugin::default(); | |
155 | data.set_data("standalone", "standalone", &standalone) | |
156 | .unwrap(); | |
157 | } | |
158 | ||
159 | Ok((PluginData { data }, digest)) | |
160 | } | |
161 | ||
162 | pub fn save_config(config: &PluginData) -> Result<(), Error> { | |
163 | super::make_acme_dir()?; | |
164 | let raw = CONFIG.write(ACME_PLUGIN_CFG_FILENAME, &config.data)?; | |
a301c362 | 165 | crate::backup::replace_backup_config(ACME_PLUGIN_CFG_FILENAME, raw.as_bytes()) |
cb67ecad WB |
166 | } |
167 | ||
168 | pub struct PluginData { | |
169 | data: SectionConfigData, | |
170 | } | |
171 | ||
172 | // And some convenience helpers. | |
173 | impl PluginData { | |
174 | pub fn remove(&mut self, name: &str) -> Option<(String, Value)> { | |
175 | self.data.sections.remove(name) | |
176 | } | |
177 | ||
178 | pub fn contains_key(&mut self, name: &str) -> bool { | |
179 | self.data.sections.contains_key(name) | |
180 | } | |
181 | ||
182 | pub fn get(&self, name: &str) -> Option<&(String, Value)> { | |
183 | self.data.sections.get(name) | |
184 | } | |
185 | ||
186 | pub fn get_mut(&mut self, name: &str) -> Option<&mut (String, Value)> { | |
187 | self.data.sections.get_mut(name) | |
188 | } | |
189 | ||
190 | pub fn insert(&mut self, id: String, ty: String, plugin: Value) { | |
191 | self.data.sections.insert(id, (ty, plugin)); | |
192 | } | |
193 | ||
194 | pub fn iter(&self) -> impl Iterator<Item = (&String, &(String, Value))> + Send { | |
195 | self.data.sections.iter() | |
196 | } | |
197 | } |