]>
Commit | Line | Data |
---|---|---|
79b902d5 | 1 | use std::collections::HashSet; |
79b902d5 | 2 | |
38b4f9b5 | 3 | use anyhow::{bail, Error}; |
79b902d5 WB |
4 | use serde::{Deserialize, Serialize}; |
5 | ||
6ef1b649 | 6 | use proxmox_schema::{api, ApiStringFormat, ApiType, Updater}; |
79b902d5 | 7 | |
1d781c5b | 8 | use proxmox_http::ProxyConfig; |
4229633d | 9 | |
af06decd | 10 | use pbs_buildcfg::configdir; |
21211748 | 11 | use pbs_config::{open_backup_lockfile, BackupLockGuard}; |
af06decd | 12 | |
f09f4d5f | 13 | use crate::acme::AcmeClient; |
72e311c6 | 14 | use crate::api2::types::{ |
f09f4d5f | 15 | AcmeAccountName, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA, |
72e311c6 | 16 | }; |
79b902d5 WB |
17 | |
18 | const CONF_FILE: &str = configdir!("/node.cfg"); | |
426847e1 | 19 | const LOCK_FILE: &str = configdir!("/.node.lck"); |
79b902d5 | 20 | |
7526d864 DM |
21 | pub fn lock() -> Result<BackupLockGuard, Error> { |
22 | open_backup_lockfile(LOCK_FILE, None, true) | |
79b902d5 WB |
23 | } |
24 | ||
25 | /// Read the Node Config. | |
26 | pub fn config() -> Result<(NodeConfig, [u8; 32]), Error> { | |
27 | let content = | |
25877d05 | 28 | proxmox_sys::fs::file_read_optional_string(CONF_FILE)?.unwrap_or_else(|| "".to_string()); |
79b902d5 WB |
29 | |
30 | let digest = openssl::sha::sha256(content.as_bytes()); | |
31 | let data: NodeConfig = crate::tools::config::from_str(&content, &NodeConfig::API_SCHEMA)?; | |
32 | ||
33 | Ok((data, digest)) | |
34 | } | |
35 | ||
36 | /// Write the Node Config, requires the write lock to be held. | |
37 | pub fn save_config(config: &NodeConfig) -> Result<(), Error> { | |
38 | config.validate()?; | |
39 | ||
40 | let raw = crate::tools::config::to_bytes(config, &NodeConfig::API_SCHEMA)?; | |
21211748 | 41 | pbs_config::replace_backup_config(CONF_FILE, &raw) |
79b902d5 WB |
42 | } |
43 | ||
44 | #[api( | |
45 | properties: { | |
39c5db7f | 46 | account: { type: AcmeAccountName }, |
79b902d5 WB |
47 | } |
48 | )] | |
49 | #[derive(Deserialize, Serialize)] | |
50 | /// The ACME configuration. | |
51 | /// | |
52 | /// Currently only contains the name of the account use. | |
53 | pub struct AcmeConfig { | |
54 | /// Account to use to acquire ACME certificates. | |
39c5db7f | 55 | account: AcmeAccountName, |
79b902d5 WB |
56 | } |
57 | ||
58 | #[api( | |
59 | properties: { | |
60 | acme: { | |
61 | optional: true, | |
62 | type: String, | |
426847e1 | 63 | format: &ApiStringFormat::PropertyString(&AcmeConfig::API_SCHEMA), |
79b902d5 WB |
64 | }, |
65 | acmedomain0: { | |
426847e1 | 66 | schema: ACME_DOMAIN_PROPERTY_SCHEMA, |
79b902d5 | 67 | optional: true, |
79b902d5 WB |
68 | }, |
69 | acmedomain1: { | |
426847e1 | 70 | schema: ACME_DOMAIN_PROPERTY_SCHEMA, |
79b902d5 | 71 | optional: true, |
79b902d5 WB |
72 | }, |
73 | acmedomain2: { | |
426847e1 | 74 | schema: ACME_DOMAIN_PROPERTY_SCHEMA, |
79b902d5 | 75 | optional: true, |
79b902d5 WB |
76 | }, |
77 | acmedomain3: { | |
426847e1 | 78 | schema: ACME_DOMAIN_PROPERTY_SCHEMA, |
79b902d5 | 79 | optional: true, |
79b902d5 WB |
80 | }, |
81 | acmedomain4: { | |
426847e1 | 82 | schema: ACME_DOMAIN_PROPERTY_SCHEMA, |
79b902d5 | 83 | optional: true, |
79b902d5 | 84 | }, |
72e311c6 DW |
85 | "http-proxy": { |
86 | schema: HTTP_PROXY_SCHEMA, | |
87 | optional: true, | |
88 | }, | |
79b902d5 WB |
89 | }, |
90 | )] | |
91 | #[derive(Deserialize, Serialize, Updater)] | |
72e311c6 | 92 | #[serde(rename_all = "kebab-case")] |
79b902d5 WB |
93 | /// Node specific configuration. |
94 | pub struct NodeConfig { | |
95 | /// The acme account to use on this node. | |
a8a20e92 DM |
96 | #[serde(skip_serializing_if = "Option::is_none")] |
97 | pub acme: Option<String>, | |
79b902d5 | 98 | |
a8a20e92 DM |
99 | #[serde(skip_serializing_if = "Option::is_none")] |
100 | pub acmedomain0: Option<String>, | |
79b902d5 | 101 | |
a8a20e92 DM |
102 | #[serde(skip_serializing_if = "Option::is_none")] |
103 | pub acmedomain1: Option<String>, | |
79b902d5 | 104 | |
a8a20e92 DM |
105 | #[serde(skip_serializing_if = "Option::is_none")] |
106 | pub acmedomain2: Option<String>, | |
79b902d5 | 107 | |
a8a20e92 DM |
108 | #[serde(skip_serializing_if = "Option::is_none")] |
109 | pub acmedomain3: Option<String>, | |
79b902d5 | 110 | |
a8a20e92 DM |
111 | #[serde(skip_serializing_if = "Option::is_none")] |
112 | pub acmedomain4: Option<String>, | |
72e311c6 | 113 | |
a8a20e92 DM |
114 | #[serde(skip_serializing_if = "Option::is_none")] |
115 | pub http_proxy: Option<String>, | |
79b902d5 WB |
116 | } |
117 | ||
118 | impl NodeConfig { | |
119 | pub fn acme_config(&self) -> Option<Result<AcmeConfig, Error>> { | |
120 | self.acme.as_deref().map(|config| -> Result<_, Error> { | |
121 | Ok(crate::tools::config::from_property_string( | |
122 | config, | |
123 | &AcmeConfig::API_SCHEMA, | |
124 | )?) | |
125 | }) | |
126 | } | |
127 | ||
128 | pub async fn acme_client(&self) -> Result<AcmeClient, Error> { | |
38b4f9b5 TL |
129 | let account = if let Some(cfg) = self.acme_config().transpose()? { |
130 | cfg.account | |
131 | } else { | |
132 | AcmeAccountName::from_string("default".to_string())? // should really not happen | |
133 | }; | |
134 | AcmeClient::load(&account).await | |
79b902d5 WB |
135 | } |
136 | ||
137 | pub fn acme_domains(&self) -> AcmeDomainIter { | |
138 | AcmeDomainIter::new(self) | |
139 | } | |
140 | ||
440472cb | 141 | /// Returns the parsed ProxyConfig |
72e311c6 DW |
142 | pub fn http_proxy(&self) -> Option<ProxyConfig> { |
143 | if let Some(http_proxy) = &self.http_proxy { | |
144 | match ProxyConfig::parse_proxy_url(&http_proxy) { | |
145 | Ok(proxy) => Some(proxy), | |
146 | Err(_) => None, | |
147 | } | |
148 | } else { | |
149 | None | |
150 | } | |
151 | } | |
152 | ||
440472cb DM |
153 | /// Sets the HTTP proxy configuration |
154 | pub fn set_http_proxy(&mut self, http_proxy: Option<String>) { | |
72e311c6 DW |
155 | self.http_proxy = http_proxy; |
156 | } | |
157 | ||
79b902d5 WB |
158 | /// Validate the configuration. |
159 | pub fn validate(&self) -> Result<(), Error> { | |
160 | let mut domains = HashSet::new(); | |
161 | for domain in self.acme_domains() { | |
162 | let domain = domain?; | |
163 | if !domains.insert(domain.domain.to_lowercase()) { | |
164 | bail!("duplicate domain '{}' in ACME config", domain.domain); | |
165 | } | |
166 | } | |
167 | ||
168 | Ok(()) | |
169 | } | |
170 | } | |
171 | ||
172 | pub struct AcmeDomainIter<'a> { | |
173 | config: &'a NodeConfig, | |
174 | index: usize, | |
175 | } | |
176 | ||
177 | impl<'a> AcmeDomainIter<'a> { | |
178 | fn new(config: &'a NodeConfig) -> Self { | |
179 | Self { config, index: 0 } | |
180 | } | |
181 | } | |
182 | ||
183 | impl<'a> Iterator for AcmeDomainIter<'a> { | |
184 | type Item = Result<AcmeDomain, Error>; | |
185 | ||
186 | fn next(&mut self) -> Option<Self::Item> { | |
187 | let domain = loop { | |
188 | let index = self.index; | |
189 | self.index += 1; | |
190 | ||
191 | let domain = match index { | |
192 | 0 => self.config.acmedomain0.as_deref(), | |
193 | 1 => self.config.acmedomain1.as_deref(), | |
194 | 2 => self.config.acmedomain2.as_deref(), | |
195 | 3 => self.config.acmedomain3.as_deref(), | |
196 | 4 => self.config.acmedomain4.as_deref(), | |
197 | _ => return None, | |
198 | }; | |
199 | ||
200 | if let Some(domain) = domain { | |
201 | break domain; | |
202 | } | |
203 | }; | |
204 | ||
205 | Some(crate::tools::config::from_property_string( | |
206 | domain, | |
207 | &AcmeDomain::API_SCHEMA, | |
208 | )) | |
209 | } | |
210 | } |