]>
Commit | Line | Data |
---|---|---|
b6be0f39 FE |
1 | use std::fmt::Display; |
2 | use std::path::{Path, PathBuf}; | |
3 | ||
fb51dcf9 | 4 | use anyhow::{format_err, Error}; |
b6be0f39 FE |
5 | use serde::{Deserialize, Serialize}; |
6 | ||
fb51dcf9 | 7 | use crate::repositories::release::DebianCodename; |
76d3a5ba FE |
8 | use crate::repositories::repository::{ |
9 | APTRepository, APTRepositoryFileType, APTRepositoryPackageType, | |
10 | }; | |
b6be0f39 | 11 | |
c7b17de1 | 12 | use proxmox_schema::api; |
b6be0f39 FE |
13 | |
14 | mod list_parser; | |
15 | use list_parser::APTListFileParser; | |
16 | ||
17 | mod sources_parser; | |
18 | use sources_parser::APTSourcesFileParser; | |
19 | ||
20 | trait APTRepositoryParser { | |
21 | /// Parse all repositories including the disabled ones and push them onto | |
22 | /// the provided vector. | |
23 | fn parse_repositories(&mut self) -> Result<Vec<APTRepository>, Error>; | |
24 | } | |
25 | ||
26 | #[api( | |
27 | properties: { | |
28 | "file-type": { | |
29 | type: APTRepositoryFileType, | |
30 | }, | |
31 | repositories: { | |
32 | description: "List of APT repositories.", | |
33 | type: Array, | |
34 | items: { | |
35 | type: APTRepository, | |
36 | }, | |
37 | }, | |
38 | digest: { | |
39 | description: "Digest for the content of the file.", | |
40 | optional: true, | |
41 | type: Array, | |
42 | items: { | |
43 | description: "Digest byte.", | |
44 | type: Integer, | |
45 | }, | |
46 | }, | |
47 | }, | |
48 | )] | |
49 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
50 | #[serde(rename_all = "kebab-case")] | |
51 | /// Represents an abstract APT repository file. | |
52 | pub struct APTRepositoryFile { | |
3e515dbc FG |
53 | /// The path to the file. If None, `contents` must be set directly. |
54 | #[serde(skip_serializing_if = "Option::is_none")] | |
55 | pub path: Option<String>, | |
b6be0f39 FE |
56 | |
57 | /// The type of the file. | |
58 | pub file_type: APTRepositoryFileType, | |
59 | ||
60 | /// List of repositories in the file. | |
61 | pub repositories: Vec<APTRepository>, | |
62 | ||
28aafe6e FG |
63 | /// The file content, if already parsed. |
64 | pub content: Option<String>, | |
65 | ||
b6be0f39 FE |
66 | /// Digest of the original contents. |
67 | pub digest: Option<[u8; 32]>, | |
68 | } | |
69 | ||
70 | #[api] | |
71 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
72 | #[serde(rename_all = "kebab-case")] | |
73 | /// Error type for problems with APT repository files. | |
74 | pub struct APTRepositoryFileError { | |
75 | /// The path to the problematic file. | |
76 | pub path: String, | |
77 | ||
78 | /// The error message. | |
79 | pub error: String, | |
80 | } | |
81 | ||
82 | impl Display for APTRepositoryFileError { | |
83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
84 | write!(f, "proxmox-apt error for '{}' - {}", self.path, self.error) | |
85 | } | |
86 | } | |
87 | ||
88 | impl std::error::Error for APTRepositoryFileError { | |
89 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
90 | None | |
91 | } | |
92 | } | |
93 | ||
76d3a5ba FE |
94 | #[api] |
95 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] | |
96 | #[serde(rename_all = "kebab-case")] | |
97 | /// Additional information for a repository. | |
98 | pub struct APTRepositoryInfo { | |
99 | /// Path to the defining file. | |
100 | #[serde(skip_serializing_if = "String::is_empty")] | |
101 | pub path: String, | |
102 | ||
103 | /// Index of the associated respository within the file (starting from 0). | |
104 | pub index: usize, | |
105 | ||
106 | /// The property from which the info originates (e.g. "Suites") | |
107 | #[serde(skip_serializing_if = "Option::is_none")] | |
108 | pub property: Option<String>, | |
109 | ||
110 | /// Info kind (e.g. "warning") | |
111 | pub kind: String, | |
112 | ||
113 | /// Info message | |
114 | pub message: String, | |
115 | } | |
116 | ||
b6be0f39 FE |
117 | impl APTRepositoryFile { |
118 | /// Creates a new `APTRepositoryFile` without parsing. | |
119 | /// | |
120 | /// If the file is hidden, the path points to a directory, or the extension | |
121 | /// is usually ignored by APT (e.g. `.orig`), `Ok(None)` is returned, while | |
122 | /// invalid file names yield an error. | |
123 | pub fn new<P: AsRef<Path>>(path: P) -> Result<Option<Self>, APTRepositoryFileError> { | |
124 | let path: PathBuf = path.as_ref().to_path_buf(); | |
125 | ||
126 | let new_err = |path_string: String, err: &str| APTRepositoryFileError { | |
127 | path: path_string, | |
128 | error: err.to_string(), | |
129 | }; | |
130 | ||
131 | let path_string = path | |
132 | .clone() | |
133 | .into_os_string() | |
134 | .into_string() | |
135 | .map_err(|os_string| { | |
136 | new_err( | |
137 | os_string.to_string_lossy().to_string(), | |
138 | "path is not valid unicode", | |
139 | ) | |
140 | })?; | |
141 | ||
142 | let new_err = |err| new_err(path_string.clone(), err); | |
143 | ||
144 | if path.is_dir() { | |
145 | return Ok(None); | |
146 | } | |
147 | ||
148 | let file_name = match path.file_name() { | |
149 | Some(file_name) => file_name | |
150 | .to_os_string() | |
151 | .into_string() | |
152 | .map_err(|_| new_err("invalid path"))?, | |
153 | None => return Err(new_err("invalid path")), | |
154 | }; | |
155 | ||
156 | if file_name.starts_with('.') || file_name.ends_with('~') { | |
157 | return Ok(None); | |
158 | } | |
159 | ||
160 | let extension = match path.extension() { | |
161 | Some(extension) => extension | |
162 | .to_os_string() | |
163 | .into_string() | |
164 | .map_err(|_| new_err("invalid path"))?, | |
165 | None => return Err(new_err("invalid extension")), | |
166 | }; | |
167 | ||
168 | // See APT's apt-pkg/init.cc | |
169 | if extension.starts_with("dpkg-") | |
170 | || extension.starts_with("ucf-") | |
171 | || matches!( | |
172 | extension.as_str(), | |
173 | "disabled" | "bak" | "save" | "orig" | "distUpgrade" | |
174 | ) | |
175 | { | |
176 | return Ok(None); | |
177 | } | |
178 | ||
179 | let file_type = APTRepositoryFileType::try_from(&extension[..]) | |
180 | .map_err(|_| new_err("invalid extension"))?; | |
181 | ||
182 | if !file_name | |
183 | .chars() | |
184 | .all(|x| x.is_ascii_alphanumeric() || x == '_' || x == '-' || x == '.') | |
185 | { | |
186 | return Err(new_err("invalid characters in file name")); | |
187 | } | |
188 | ||
189 | Ok(Some(Self { | |
3e515dbc | 190 | path: Some(path_string), |
b6be0f39 FE |
191 | file_type, |
192 | repositories: vec![], | |
193 | digest: None, | |
28aafe6e | 194 | content: None, |
b6be0f39 FE |
195 | })) |
196 | } | |
197 | ||
28aafe6e FG |
198 | pub fn with_content(content: String, content_type: APTRepositoryFileType) -> Self { |
199 | Self { | |
200 | file_type: content_type, | |
201 | content: Some(content), | |
3e515dbc | 202 | path: None, |
28aafe6e FG |
203 | repositories: vec![], |
204 | digest: None, | |
205 | } | |
206 | } | |
207 | ||
b6be0f39 FE |
208 | /// Check if the file exists. |
209 | pub fn exists(&self) -> bool { | |
3e515dbc FG |
210 | if let Some(path) = &self.path { |
211 | PathBuf::from(path).exists() | |
28aafe6e FG |
212 | } else { |
213 | false | |
214 | } | |
b6be0f39 FE |
215 | } |
216 | ||
217 | pub fn read_with_digest(&self) -> Result<(Vec<u8>, [u8; 32]), APTRepositoryFileError> { | |
3e515dbc FG |
218 | if let Some(path) = &self.path { |
219 | let content = std::fs::read(path).map_err(|err| self.err(format_err!("{}", err)))?; | |
28aafe6e FG |
220 | let digest = openssl::sha::sha256(&content); |
221 | ||
222 | Ok((content, digest)) | |
223 | } else if let Some(ref content) = self.content { | |
224 | let content = content.as_bytes(); | |
225 | let digest = openssl::sha::sha256(content); | |
226 | Ok((content.to_vec(), digest)) | |
227 | } else { | |
228 | Err(self.err(format_err!( | |
229 | "Neither 'path' nor 'content' set, cannot read APT repository info." | |
230 | ))) | |
231 | } | |
b6be0f39 FE |
232 | } |
233 | ||
234 | /// Create an `APTRepositoryFileError`. | |
235 | pub fn err(&self, error: Error) -> APTRepositoryFileError { | |
236 | APTRepositoryFileError { | |
3e515dbc | 237 | path: self.path.clone().unwrap_or_default(), |
b6be0f39 FE |
238 | error: error.to_string(), |
239 | } | |
240 | } | |
241 | ||
242 | /// Parses the APT repositories configured in the file on disk, including | |
243 | /// disabled ones. | |
244 | /// | |
245 | /// Resets the current repositories and digest, even on failure. | |
246 | pub fn parse(&mut self) -> Result<(), APTRepositoryFileError> { | |
247 | self.repositories.clear(); | |
248 | self.digest = None; | |
249 | ||
250 | let (content, digest) = self.read_with_digest()?; | |
251 | ||
252 | let mut parser: Box<dyn APTRepositoryParser> = match self.file_type { | |
253 | APTRepositoryFileType::List => Box::new(APTListFileParser::new(&content[..])), | |
254 | APTRepositoryFileType::Sources => Box::new(APTSourcesFileParser::new(&content[..])), | |
255 | }; | |
256 | ||
257 | let repos = parser.parse_repositories().map_err(|err| self.err(err))?; | |
258 | ||
259 | for (n, repo) in repos.iter().enumerate() { | |
260 | repo.basic_check() | |
261 | .map_err(|err| self.err(format_err!("check for repository {} - {}", n + 1, err)))?; | |
262 | } | |
263 | ||
264 | self.repositories = repos; | |
265 | self.digest = Some(digest); | |
266 | ||
267 | Ok(()) | |
268 | } | |
269 | ||
270 | /// Writes the repositories to the file on disk. | |
271 | /// | |
272 | /// If a digest is provided, checks that the current content of the file still | |
273 | /// produces the same one. | |
274 | pub fn write(&self) -> Result<(), APTRepositoryFileError> { | |
3e515dbc FG |
275 | let path = match &self.path { |
276 | Some(path) => path, | |
277 | None => { | |
278 | return Err(self.err(format_err!( | |
279 | "Cannot write to APT repository file without path." | |
280 | ))); | |
281 | } | |
282 | }; | |
28aafe6e | 283 | |
b6be0f39 FE |
284 | if let Some(digest) = self.digest { |
285 | if !self.exists() { | |
286 | return Err(self.err(format_err!("digest specified, but file does not exist"))); | |
287 | } | |
288 | ||
289 | let (_, current_digest) = self.read_with_digest()?; | |
290 | if digest != current_digest { | |
291 | return Err(self.err(format_err!("digest mismatch"))); | |
292 | } | |
293 | } | |
294 | ||
295 | if self.repositories.is_empty() { | |
3e515dbc | 296 | return std::fs::remove_file(&path) |
b6be0f39 FE |
297 | .map_err(|err| self.err(format_err!("unable to remove file - {}", err))); |
298 | } | |
299 | ||
300 | let mut content = vec![]; | |
301 | ||
302 | for (n, repo) in self.repositories.iter().enumerate() { | |
303 | repo.basic_check() | |
304 | .map_err(|err| self.err(format_err!("check for repository {} - {}", n + 1, err)))?; | |
305 | ||
306 | repo.write(&mut content) | |
307 | .map_err(|err| self.err(format_err!("writing repository {} - {}", n + 1, err)))?; | |
308 | } | |
309 | ||
3e515dbc | 310 | let path = PathBuf::from(&path); |
b6be0f39 FE |
311 | let dir = match path.parent() { |
312 | Some(dir) => dir, | |
313 | None => return Err(self.err(format_err!("invalid path"))), | |
314 | }; | |
315 | ||
316 | std::fs::create_dir_all(dir) | |
317 | .map_err(|err| self.err(format_err!("unable to create parent dir - {}", err)))?; | |
318 | ||
319 | let pid = std::process::id(); | |
320 | let mut tmp_path = path.clone(); | |
321 | tmp_path.set_extension("tmp"); | |
322 | tmp_path.set_extension(format!("{}", pid)); | |
323 | ||
324 | if let Err(err) = std::fs::write(&tmp_path, content) { | |
325 | let _ = std::fs::remove_file(&tmp_path); | |
326 | return Err(self.err(format_err!("writing {:?} failed - {}", path, err))); | |
327 | } | |
328 | ||
329 | if let Err(err) = std::fs::rename(&tmp_path, &path) { | |
330 | let _ = std::fs::remove_file(&tmp_path); | |
331 | return Err(self.err(format_err!("rename failed for {:?} - {}", path, err))); | |
332 | } | |
333 | ||
334 | Ok(()) | |
335 | } | |
76d3a5ba | 336 | |
037ce3a0 FE |
337 | /// Checks if old or unstable suites are configured and that the Debian security repository |
338 | /// has the correct suite. Also checks that the `stable` keyword is not used. | |
fb51dcf9 | 339 | pub fn check_suites(&self, current_codename: DebianCodename) -> Vec<APTRepositoryInfo> { |
76d3a5ba FE |
340 | let mut infos = vec![]; |
341 | ||
3e515dbc FG |
342 | let path = match &self.path { |
343 | Some(path) => path.clone(), | |
344 | None => return vec![], | |
345 | }; | |
346 | ||
76d3a5ba | 347 | for (n, repo) in self.repositories.iter().enumerate() { |
5581858b | 348 | if !repo.types.contains(&APTRepositoryPackageType::Deb) { |
76d3a5ba FE |
349 | continue; |
350 | } | |
351 | ||
037ce3a0 FE |
352 | let is_security_repo = repo.uris.iter().any(|uri| { |
353 | let uri = uri.trim_end_matches('/'); | |
354 | let uri = uri.strip_suffix("debian-security").unwrap_or(uri); | |
355 | let uri = uri.trim_end_matches('/'); | |
356 | matches!( | |
357 | uri, | |
358 | "http://security.debian.org" | "https://security.debian.org", | |
359 | ) | |
360 | }); | |
361 | ||
362 | let require_suffix = match is_security_repo { | |
363 | true if current_codename >= DebianCodename::Bullseye => Some("-security"), | |
364 | true => Some("/updates"), | |
365 | false => None, | |
366 | }; | |
367 | ||
fb51dcf9 | 368 | let mut add_info = |kind: &str, message| { |
76d3a5ba | 369 | infos.push(APTRepositoryInfo { |
3e515dbc | 370 | path: path.clone(), |
76d3a5ba FE |
371 | index: n, |
372 | property: Some("Suites".to_string()), | |
fb51dcf9 | 373 | kind: kind.to_string(), |
76d3a5ba FE |
374 | message, |
375 | }) | |
376 | }; | |
377 | ||
fb51dcf9 FE |
378 | let message_old = |suite| format!("old suite '{}' configured!", suite); |
379 | let message_new = | |
380 | |suite| format!("suite '{}' should not be used in production!", suite); | |
381 | let message_stable = "use the name of the stable distribution instead of 'stable'!"; | |
76d3a5ba | 382 | |
51c69d76 | 383 | for suite in repo.suites.iter() { |
037ce3a0 | 384 | let (base_suite, suffix) = suite_variant(suite); |
51c69d76 | 385 | |
fb51dcf9 FE |
386 | match base_suite { |
387 | "oldoldstable" | "oldstable" => { | |
388 | add_info("warning", message_old(base_suite)); | |
76d3a5ba | 389 | } |
fb51dcf9 FE |
390 | "testing" | "unstable" | "experimental" | "sid" => { |
391 | add_info("warning", message_new(base_suite)); | |
76d3a5ba | 392 | } |
fb51dcf9 FE |
393 | "stable" => { |
394 | add_info("warning", message_stable.to_string()); | |
76d3a5ba | 395 | } |
fb51dcf9 FE |
396 | _ => (), |
397 | }; | |
398 | ||
399 | let codename: DebianCodename = match base_suite.try_into() { | |
400 | Ok(codename) => codename, | |
401 | Err(_) => continue, | |
402 | }; | |
403 | ||
404 | if codename < current_codename { | |
405 | add_info("warning", message_old(base_suite)); | |
406 | } | |
407 | ||
408 | if Some(codename) == current_codename.next() { | |
409 | add_info("ignore-pre-upgrade-warning", message_new(base_suite)); | |
410 | } else if codename > current_codename { | |
411 | add_info("warning", message_new(base_suite)); | |
76d3a5ba | 412 | } |
037ce3a0 FE |
413 | |
414 | if let Some(require_suffix) = require_suffix { | |
415 | if suffix != require_suffix { | |
416 | add_info( | |
417 | "warning", | |
418 | format!("expected suite '{}{}'", current_codename, require_suffix), | |
419 | ); | |
420 | } | |
421 | } | |
76d3a5ba | 422 | } |
76d3a5ba FE |
423 | } |
424 | ||
fb51dcf9 | 425 | infos |
76d3a5ba FE |
426 | } |
427 | ||
428 | /// Checks for official URIs. | |
429 | pub fn check_uris(&self) -> Vec<APTRepositoryInfo> { | |
430 | let mut infos = vec![]; | |
431 | ||
3e515dbc FG |
432 | let path = match &self.path { |
433 | Some(path) => path, | |
434 | None => return vec![], | |
435 | }; | |
436 | ||
76d3a5ba | 437 | for (n, repo) in self.repositories.iter().enumerate() { |
87ea23ec FE |
438 | let mut origin = match repo.get_cached_origin() { |
439 | Ok(option) => option, | |
440 | Err(_) => None, | |
441 | }; | |
442 | ||
443 | if origin.is_none() { | |
444 | origin = repo.origin_from_uris(); | |
445 | } | |
446 | ||
447 | if let Some(origin) = origin { | |
76d3a5ba | 448 | infos.push(APTRepositoryInfo { |
3e515dbc | 449 | path: path.clone(), |
76d3a5ba | 450 | index: n, |
87ea23ec FE |
451 | kind: "origin".to_string(), |
452 | property: None, | |
453 | message: origin, | |
76d3a5ba FE |
454 | }); |
455 | } | |
456 | } | |
457 | ||
458 | infos | |
459 | } | |
b6be0f39 | 460 | } |
51c69d76 FE |
461 | |
462 | /// Splits the suite into its base part and variant. | |
463 | /// Does not expect the base part to contain either `-` or `/`. | |
464 | fn suite_variant(suite: &str) -> (&str, &str) { | |
465 | match suite.find(&['-', '/'][..]) { | |
466 | Some(n) => (&suite[0..n], &suite[n..]), | |
467 | None => (suite, ""), | |
468 | } | |
469 | } |