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