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