]> git.proxmox.com Git - proxmox-backup.git/blame - src/tools.rs
fix #3359: fix blocking writes in async code during pxar create
[proxmox-backup.git] / src / tools.rs
CommitLineData
51b499db
DM
1//! Tools and utilities
2//!
3//! This is a collection of small and useful tools.
6100071f 4use std::any::Any;
61653382 5use std::borrow::Borrow;
6100071f 6use std::collections::HashMap;
62ee2eb4 7use std::hash::BuildHasher;
98c259b4 8use std::fs::File;
b649887e 9use std::io::{self, BufRead, Read, Seek, SeekFrom};
98c259b4 10use std::os::unix::io::RawFd;
6100071f 11use std::path::Path;
365bb90f 12
f7d4e4b5 13use anyhow::{bail, format_err, Error};
af926291 14use serde_json::Value;
c5946faf 15use openssl::hash::{hash, DigestBytes, MessageDigest};
968a0ab2 16use percent_encoding::{utf8_percent_encode, AsciiSet};
0fe5d605 17
00ec8d16
WB
18pub use proxmox::tools::fd::Fd;
19
6100071f 20pub mod acl;
e6513bd5 21pub mod apt;
556eb70e 22pub mod async_io;
6ed25cbe 23pub mod borrow;
ec01eead 24pub mod cert;
6100071f 25pub mod daemon;
10effc98 26pub mod disks;
4939255f 27pub mod format;
fb01fd3a
WB
28pub mod fs;
29pub mod fuse_loop;
30pub mod http;
9ff747ef 31pub mod json;
8074d2b0 32pub mod logrotate;
45f9b32e 33pub mod loopdev;
fb01fd3a
WB
34pub mod lru_cache;
35pub mod nom;
36pub mod runtime;
59e94227 37pub mod serde_filter;
97168f92 38pub mod socket;
fb01fd3a 39pub mod statistics;
7b22fb25 40pub mod subscription;
fb01fd3a
WB
41pub mod systemd;
42pub mod ticket;
43pub mod xattr;
fdce52aa 44pub mod zip;
9372c078 45pub mod sgutils2;
639a6782 46pub mod paperkey;
f1d99e3f 47
fb01fd3a
WB
48pub mod parallel_handler;
49pub use parallel_handler::ParallelHandler;
3c9b3702 50
f1d99e3f 51mod wrapped_reader_stream;
fb01fd3a 52pub use wrapped_reader_stream::{AsyncReaderStream, StdChannelStream, WrappedReaderStream};
dcd033a5 53
943479f5 54mod async_channel_writer;
fb01fd3a 55pub use async_channel_writer::AsyncChannelWriter;
943479f5 56
dcd033a5 57mod std_channel_writer;
fb01fd3a 58pub use std_channel_writer::StdChannelWriter;
8cf6e764 59
f1d76ecf
DC
60mod tokio_writer_adapter;
61pub use tokio_writer_adapter::TokioWriterAdapter;
62
a650f503 63mod process_locker;
fb01fd3a 64pub use process_locker::{ProcessLocker, ProcessLockExclusiveGuard, ProcessLockSharedGuard};
a650f503 65
3b151414 66mod file_logger;
fb01fd3a 67pub use file_logger::{FileLogger, FileLogOptions};
3b151414 68
490be29e 69mod broadcast_future;
fb01fd3a 70pub use broadcast_future::{BroadcastData, BroadcastFuture};
490be29e 71
fded74d0 72/// The `BufferedRead` trait provides a single function
0a72e267
DM
73/// `buffered_read`. It returns a reference to an internal buffer. The
74/// purpose of this traid is to avoid unnecessary data copies.
fded74d0 75pub trait BufferedRead {
318564ac
DM
76 /// This functions tries to fill the internal buffers, then
77 /// returns a reference to the available data. It returns an empty
78 /// buffer if `offset` points to the end of the file.
0a72e267
DM
79 fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>;
80}
81
f5f13ebc 82pub fn json_object_to_query(data: Value) -> Result<String, Error> {
f5f13ebc
DM
83 let mut query = url::form_urlencoded::Serializer::new(String::new());
84
85 let object = data.as_object().ok_or_else(|| {
86 format_err!("json_object_to_query: got wrong data type (expected object).")
87 })?;
88
89 for (key, value) in object {
90 match value {
6100071f
WB
91 Value::Bool(b) => {
92 query.append_pair(key, &b.to_string());
93 }
94 Value::Number(n) => {
95 query.append_pair(key, &n.to_string());
96 }
97 Value::String(s) => {
98 query.append_pair(key, &s);
99 }
f5f13ebc
DM
100 Value::Array(arr) => {
101 for element in arr {
102 match element {
6100071f
WB
103 Value::Bool(b) => {
104 query.append_pair(key, &b.to_string());
105 }
106 Value::Number(n) => {
107 query.append_pair(key, &n.to_string());
108 }
109 Value::String(s) => {
110 query.append_pair(key, &s);
111 }
112 _ => bail!(
113 "json_object_to_query: unable to handle complex array data types."
114 ),
f5f13ebc
DM
115 }
116 }
117 }
118 _ => bail!("json_object_to_query: unable to handle complex data types."),
119 }
120 }
121
122 Ok(query.finish())
123}
124
0fe5d605 125pub fn required_string_param<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> {
6100071f 126 match param[name].as_str() {
0fe5d605
DM
127 Some(s) => Ok(s),
128 None => bail!("missing parameter '{}'", name),
129 }
130}
0d38dcb4 131
e17d5d86
DM
132pub fn required_string_property<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> {
133 match param[name].as_str() {
134 Some(s) => Ok(s),
135 None => bail!("missing property '{}'", name),
136 }
137}
138
a4ba60be 139pub fn required_integer_param(param: &Value, name: &str) -> Result<i64, Error> {
6100071f 140 match param[name].as_i64() {
0d38dcb4
DM
141 Some(s) => Ok(s),
142 None => bail!("missing parameter '{}'", name),
f8dfbb45
DM
143 }
144}
145
a4ba60be 146pub fn required_integer_property(param: &Value, name: &str) -> Result<i64, Error> {
e17d5d86
DM
147 match param[name].as_i64() {
148 Some(s) => Ok(s),
149 None => bail!("missing property '{}'", name),
150 }
151}
152
35304303 153pub fn required_array_param<'a>(param: &'a Value, name: &str) -> Result<&'a [Value], Error> {
6100071f 154 match param[name].as_array() {
35304303 155 Some(s) => Ok(&s),
f8dfbb45 156 None => bail!("missing parameter '{}'", name),
0d38dcb4
DM
157 }
158}
383e8577 159
35304303 160pub fn required_array_property<'a>(param: &'a Value, name: &str) -> Result<&'a [Value], Error> {
e17d5d86 161 match param[name].as_array() {
35304303 162 Some(s) => Ok(&s),
e17d5d86
DM
163 None => bail!("missing property '{}'", name),
164 }
165}
166
a4ba60be
WB
167pub fn complete_file_name<S>(arg: &str, _param: &HashMap<String, String, S>) -> Vec<String>
168where
169 S: BuildHasher,
170{
383e8577
DM
171 let mut result = vec![];
172
6100071f 173 use nix::fcntl::AtFlags;
383e8577
DM
174 use nix::fcntl::OFlag;
175 use nix::sys::stat::Mode;
383e8577 176
62ee2eb4 177 let mut dirname = std::path::PathBuf::from(if arg.is_empty() { "./" } else { arg });
383e8577
DM
178
179 let is_dir = match nix::sys::stat::fstatat(libc::AT_FDCWD, &dirname, AtFlags::empty()) {
180 Ok(stat) => (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR,
181 Err(_) => false,
182 };
183
184 if !is_dir {
185 if let Some(parent) = dirname.parent() {
186 dirname = parent.to_owned();
187 }
188 }
189
6100071f
WB
190 let mut dir =
191 match nix::dir::Dir::openat(libc::AT_FDCWD, &dirname, OFlag::O_DIRECTORY, Mode::empty()) {
192 Ok(d) => d,
193 Err(_) => return result,
194 };
383e8577
DM
195
196 for item in dir.iter() {
197 if let Ok(entry) = item {
198 if let Ok(name) = entry.file_name().to_str() {
6100071f
WB
199 if name == "." || name == ".." {
200 continue;
201 }
383e8577
DM
202 let mut newpath = dirname.clone();
203 newpath.push(name);
204
6100071f
WB
205 if let Ok(stat) =
206 nix::sys::stat::fstatat(libc::AT_FDCWD, &newpath, AtFlags::empty())
207 {
383e8577
DM
208 if (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR {
209 newpath.push("");
210 if let Some(newpath) = newpath.to_str() {
211 result.push(newpath.to_owned());
212 }
213 continue;
6100071f 214 }
383e8577
DM
215 }
216 if let Some(newpath) = newpath.to_str() {
217 result.push(newpath.to_owned());
218 }
6100071f 219 }
383e8577
DM
220 }
221 }
222
223 result
224}
443f3743
DM
225
226/// Scan directory for matching file names.
227///
228/// Scan through all directory entries and call `callback()` function
229/// if the entry name matches the regular expression. This function
230/// used unix `openat()`, so you can pass absolute or relative file
231/// names. This function simply skips non-UTF8 encoded names.
232pub fn scandir<P, F>(
233 dirfd: RawFd,
121f18ef 234 path: &P,
443f3743 235 regex: &regex::Regex,
6100071f 236 mut callback: F,
443f3743 237) -> Result<(), Error>
6100071f
WB
238where
239 F: FnMut(RawFd, &str, nix::dir::Type) -> Result<(), Error>,
240 P: ?Sized + nix::NixPath,
443f3743 241{
121f18ef 242 for entry in self::fs::scan_subdir(dirfd, path, regex)? {
443f3743
DM
243 let entry = entry?;
244 let file_type = match entry.file_type() {
245 Some(file_type) => file_type,
246 None => bail!("unable to detect file type"),
247 };
443f3743 248
6100071f
WB
249 callback(
250 entry.parent_fd(),
251 unsafe { entry.file_name_utf8_unchecked() },
252 file_type,
253 )?;
443f3743
DM
254 }
255 Ok(())
256}
7e13b2d6 257
c5946faf
WB
258/// Shortcut for md5 sums.
259pub fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> {
260 hash(MessageDigest::md5(), data).map_err(Error::from)
261}
262
7e13b2d6 263pub fn get_hardware_address() -> Result<String, Error> {
1631c54f 264 static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub";
7e13b2d6 265
72c0e102
TL
266 let contents = proxmox::tools::fs::file_get_contents(FILENAME)
267 .map_err(|e| format_err!("Error getting host key - {}", e))?;
268 let digest = md5sum(&contents)
269 .map_err(|e| format_err!("Error digesting host key - {}", e))?;
7e13b2d6 270
52fe9e8e 271 Ok(proxmox::tools::bin_to_hex(&digest).to_uppercase())
7e13b2d6 272}
22968600 273
af2fddea
DM
274pub fn assert_if_modified(digest1: &str, digest2: &str) -> Result<(), Error> {
275 if digest1 != digest2 {
6100071f 276 bail!("detected modified configuration - file changed by other user? Try again.");
af2fddea
DM
277 }
278 Ok(())
279}
b9903d63 280
09f12d1c 281/// Extract a specific cookie from cookie header.
b9903d63 282/// We assume cookie_name is already url encoded.
09f12d1c 283pub fn extract_cookie(cookie: &str, cookie_name: &str) -> Option<String> {
b9903d63 284 for pair in cookie.split(';') {
b9903d63
DM
285 let (name, value) = match pair.find('=') {
286 Some(i) => (pair[..i].trim(), pair[(i + 1)..].trim()),
287 None => return None, // Cookie format error
288 };
289
290 if name == cookie_name {
8a1028e0 291 use percent_encoding::percent_decode;
b9903d63
DM
292 if let Ok(value) = percent_decode(value.as_bytes()).decode_utf8() {
293 return Some(value.into());
294 } else {
295 return None; // Cookie format error
296 }
297 }
298 }
299
300 None
301}
af53186e 302
968a0ab2
DC
303/// percent encode a url component
304pub fn percent_encode_component(comp: &str) -> String {
305 utf8_percent_encode(comp, percent_encoding::NON_ALPHANUMERIC).to_string()
306}
307
61653382 308pub fn join<S: Borrow<str>>(data: &[S], sep: char) -> String {
af53186e
DM
309 let mut list = String::new();
310
311 for item in data {
62ee2eb4 312 if !list.is_empty() {
6100071f
WB
313 list.push(sep);
314 }
61653382 315 list.push_str(item.borrow());
af53186e
DM
316 }
317
318 list
319}
ff7049d4 320
002a191a
DM
321/// Detect modified configuration files
322///
add5861e 323/// This function fails with a reasonable error message if checksums do not match.
002a191a
DM
324pub fn detect_modified_configuration_file(digest1: &[u8;32], digest2: &[u8;32]) -> Result<(), Error> {
325 if digest1 != digest2 {
a4ba60be 326 bail!("detected modified configuration - file changed by other user? Try again.");
002a191a
DM
327 }
328 Ok(())
329}
330
3578d99f
DM
331/// normalize uri path
332///
333/// Do not allow ".", "..", or hidden files ".XXXX"
334/// Also remove empty path components
335pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> {
3578d99f
DM
336 let items = path.split('/');
337
338 let mut path = String::new();
339 let mut components = vec![];
340
341 for name in items {
6100071f
WB
342 if name.is_empty() {
343 continue;
344 }
62ee2eb4 345 if name.starts_with('.') {
3578d99f
DM
346 bail!("Path contains illegal components.");
347 }
348 path.push('/');
349 path.push_str(name);
350 components.push(name);
351 }
352
353 Ok((path, components))
354}
355
97fab7aa 356/// Helper to check result from std::process::Command output
143b6545
DM
357///
358/// The exit_code_check() function should return true if the exit code
359/// is considered successful.
360pub fn command_output(
361 output: std::process::Output,
144006fa 362 exit_code_check: Option<fn(i32) -> bool>,
e64b9f92 363) -> Result<Vec<u8>, Error> {
97fab7aa
DM
364
365 if !output.status.success() {
366 match output.status.code() {
367 Some(code) => {
143b6545
DM
368 let is_ok = match exit_code_check {
369 Some(check_fn) => check_fn(code),
370 None => code == 0,
371 };
372 if !is_ok {
97fab7aa
DM
373 let msg = String::from_utf8(output.stderr)
374 .map(|m| if m.is_empty() { String::from("no error message") } else { m })
375 .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
376
377 bail!("status code: {} - {}", code, msg);
378 }
379 }
380 None => bail!("terminated by signal"),
381 }
382 }
383
e64b9f92
DM
384 Ok(output.stdout)
385}
97fab7aa 386
e64b9f92
DM
387/// Helper to check result from std::process::Command output, returns String.
388///
389/// The exit_code_check() function should return true if the exit code
390/// is considered successful.
391pub fn command_output_as_string(
392 output: std::process::Output,
393 exit_code_check: Option<fn(i32) -> bool>,
394) -> Result<String, Error> {
395 let output = command_output(output, exit_code_check)?;
396 let output = String::from_utf8(output)?;
97fab7aa
DM
397 Ok(output)
398}
399
144006fa
DM
400pub fn run_command(
401 mut command: std::process::Command,
402 exit_code_check: Option<fn(i32) -> bool>,
403) -> Result<String, Error> {
404
405 let output = command.output()
406 .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?;
407
f254a270 408 let output = command_output_as_string(output, exit_code_check)
144006fa
DM
409 .map_err(|err| format_err!("command {:?} failed - {}", command, err))?;
410
411 Ok(output)
412}
97fab7aa 413
ff7049d4 414pub fn fd_change_cloexec(fd: RawFd, on: bool) -> Result<(), Error> {
6100071f 415 use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
ff7049d4
WB
416 let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?)
417 .ok_or_else(|| format_err!("unhandled file flags"))?; // nix crate is stupid this way...
418 flags.set(FdFlag::FD_CLOEXEC, on);
419 fcntl(fd, F_SETFD(flags))?;
420 Ok(())
421}
9136f857 422
9136f857
DM
423static mut SHUTDOWN_REQUESTED: bool = false;
424
425pub fn request_shutdown() {
6100071f
WB
426 unsafe {
427 SHUTDOWN_REQUESTED = true;
428 }
7a630df7 429 crate::server::server_shutdown();
9136f857
DM
430}
431
432#[inline(always)]
433pub fn shutdown_requested() -> bool {
434 unsafe { SHUTDOWN_REQUESTED }
435}
92da93b2
DM
436
437pub fn fail_on_shutdown() -> Result<(), Error> {
438 if shutdown_requested() {
439 bail!("Server shutdown requested - aborting task");
440 }
441 Ok(())
442}
d96bb7f1 443
c4044009
WB
444/// safe wrapper for `nix::unistd::pipe2` defaulting to `O_CLOEXEC` and guarding the file
445/// descriptors.
efd1536e
WB
446pub fn pipe() -> Result<(Fd, Fd), Error> {
447 let (pin, pout) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?;
448 Ok((Fd(pin), Fd(pout)))
449}
2edc341b 450
c4044009
WB
451/// safe wrapper for `nix::sys::socket::socketpair` defaulting to `O_CLOEXEC` and guarding the file
452/// descriptors.
453pub fn socketpair() -> Result<(Fd, Fd), Error> {
454 use nix::sys::socket;
455 let (pa, pb) = socket::socketpair(
456 socket::AddressFamily::Unix,
457 socket::SockType::Stream,
458 None,
459 socket::SockFlag::SOCK_CLOEXEC,
460 )?;
461 Ok((Fd(pa), Fd(pb)))
462}
463
464
2edc341b
DM
465/// An easy way to convert types to Any
466///
467/// Mostly useful to downcast trait objects (see RpcEnvironment).
468pub trait AsAny {
dd5495d6 469 fn as_any(&self) -> &dyn Any;
2edc341b
DM
470}
471
472impl<T: Any> AsAny for T {
6100071f
WB
473 fn as_any(&self) -> &dyn Any {
474 self
475 }
2edc341b 476}
8a1028e0
WB
477
478/// This used to be: `SIMPLE_ENCODE_SET` plus space, `"`, `#`, `<`, `>`, backtick, `?`, `{`, `}`
479pub const DEFAULT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS // 0..1f and 7e
480 // The SIMPLE_ENCODE_SET adds space and anything >= 0x7e (7e itself is already included above)
481 .add(0x20)
482 .add(0x7f)
483 // the DEFAULT_ENCODE_SET added:
484 .add(b' ')
485 .add(b'"')
486 .add(b'#')
487 .add(b'<')
488 .add(b'>')
489 .add(b'`')
490 .add(b'?')
491 .add(b'{')
492 .add(b'}');
386990ba
WB
493
494/// Get an iterator over lines of a file, skipping empty lines and comments (lines starting with a
495/// `#`).
496pub fn file_get_non_comment_lines<P: AsRef<Path>>(
497 path: P,
498) -> Result<impl Iterator<Item = io::Result<String>>, Error> {
499 let path = path.as_ref();
500
501 Ok(io::BufReader::new(
502 File::open(path).map_err(|err| format_err!("error opening {:?}: {}", path, err))?,
503 )
504 .lines()
505 .filter_map(|line| match line {
506 Ok(line) => {
507 let line = line.trim();
508 if line.is_empty() || line.starts_with('#') {
509 None
510 } else {
511 Some(Ok(line.to_string()))
512 }
513 }
514 Err(err) => Some(Err(err)),
515 }))
516}
e693818a 517
ac7513e3
DM
518pub fn setup_safe_path_env() {
519 std::env::set_var("PATH", "/sbin:/bin:/usr/sbin:/usr/bin");
520 // Make %ENV safer - as suggested by https://perldoc.perl.org/perlsec.html
521 for name in &["IFS", "CDPATH", "ENV", "BASH_ENV"] {
522 std::env::remove_var(name);
523 }
524}
cdf1da28
WB
525
526pub fn strip_ascii_whitespace(line: &[u8]) -> &[u8] {
527 let line = match line.iter().position(|&b| !b.is_ascii_whitespace()) {
528 Some(n) => &line[n..],
529 None => return &[],
530 };
531 match line.iter().rev().position(|&b| !b.is_ascii_whitespace()) {
532 Some(n) => &line[..(line.len() - n)],
533 None => &[],
534 }
535}
1bc1d81a
DM
536
537/// Seeks to start of file and computes the SHA256 hash
538pub fn compute_file_csum(file: &mut File) -> Result<([u8; 32], u64), Error> {
539
540 file.seek(SeekFrom::Start(0))?;
541
542 let mut hasher = openssl::sha::Sha256::new();
543 let mut buffer = proxmox::tools::vec::undefined(256*1024);
544 let mut size: u64 = 0;
545
546 loop {
547 let count = match file.read(&mut buffer) {
a4ba60be 548 Ok(0) => break,
1bc1d81a
DM
549 Ok(count) => count,
550 Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => {
551 continue;
552 }
553 Err(err) => return Err(err.into()),
554 };
1bc1d81a
DM
555 size += count as u64;
556 hasher.update(&buffer[..count]);
557 }
558
559 let csum = hasher.finish();
560
561 Ok((csum, size))
562}
014dc5f9
WB
563
564/// Create the base run-directory.
565///
566/// This exists to fixate the permissions for the run *base* directory while allowing intermediate
567/// directories after it to have different permissions.
568pub fn create_run_dir() -> Result<(), Error> {
569 let _: bool = proxmox::tools::fs::create_path(PROXMOX_BACKUP_RUN_DIR_M!(), None, None)?;
570 Ok(())
571}