diff --git a/Cargo.toml b/Cargo.toml index da4e27ad..b62fcd50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "proxmox-rrd-api-types", "proxmox-schema", "proxmox-section-config", + "proxmox-sendmail", "proxmox-serde", "proxmox-shared-cache", "proxmox-shared-memory", @@ -120,12 +121,12 @@ zstd = { version = "0.12", features = [ "bindgen" ] } # workspace dependencies proxmox-acme = { version = "0.5.3", path = "proxmox-acme", default-features = false } proxmox-api-macro = { version = "1.2.0", path = "proxmox-api-macro" } -proxmox-apt-api-types = { version = "1.0.1", path = "proxmox-apt-api-types" } +proxmox-apt-api-types = { version = "1.0.2", path = "proxmox-apt-api-types" } proxmox-auth-api = { version = "0.4.0", path = "proxmox-auth-api" } proxmox-async = { version = "0.4.1", path = "proxmox-async" } proxmox-compression = { version = "0.2.4", path = "proxmox-compression" } proxmox-daemon = { version = "0.1.0", path = "proxmox-daemon" } -proxmox-http = { version = "0.9.2", path = "proxmox-http" } +proxmox-http = { version = "0.9.4", path = "proxmox-http" } proxmox-http-error = { version = "0.1.0", path = "proxmox-http-error" } proxmox-human-byte = { version = "0.1.0", path = "proxmox-human-byte" } proxmox-io = { version = "1.1.0", path = "proxmox-io" } @@ -138,6 +139,7 @@ proxmox-rest-server = { version = "0.8.0", path = "proxmox-rest-server" } proxmox-router = { version = "3.0.0", path = "proxmox-router" } proxmox-schema = { version = "3.1.2", path = "proxmox-schema" } proxmox-section-config = { version = "2.1.0", path = "proxmox-section-config" } +proxmox-sendmail = { version = "0.1.0", path = "proxmox-sendmail" } proxmox-serde = { version = "0.1.1", path = "proxmox-serde", features = [ "serde_json" ] } proxmox-shared-memory = { version = "0.3.0", path = "proxmox-shared-memory" } proxmox-sortable-macro = { version = "0.1.3", path = "proxmox-sortable-macro" } diff --git a/Makefile b/Makefile index e12427d3..6ee59782 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,8 @@ install-overlay: $(foreach c,$(CRATES), $c-install-overlay) fakeroot $(MAKE) $*-sysext-do %-sysext-do: rm -f extensions/$*.raw + rm -rf build/sysext/$* + rm -rf build/install/$* $(MAKE) DESTDIR=build/sysext/$* $*-install-overlay mkdir -p extensions mkfs.erofs extensions/$*.raw build/sysext/$* diff --git a/README.md b/README.md index 84b097e1..3f4e85de 100644 --- a/README.md +++ b/README.md @@ -95,3 +95,59 @@ Some restrictions apply: - workspace dependency specifications cannot include `optional` - if needed, the `optional` flag needs to be set at the member level when using a workspace dependency + +# Working with *other* projects while changing to *single crates here* + +When crates from this workspace need changes caused by requirements in projects +*outside* of this repository, it can often be annoying to keep building and +installing `.deb` files. + +Additionally, doing so often requires complete rebuilds as cargo will not pick +up *file* changes of external dependencies. + +One way to fix this is by actually changing the version. Since we cut away +anything starting at the first hyphen in the version, we need to use a `+` +(build metadata) version suffix. + +Eg. turn `5.0.0` into `5.0.0+test8`. + +There are 2 faster ways: + +## Adding a `#[patch.crates-io]` section to the other project. + +Note, however, that this requires *ALL* crates from this workspace to be listed, +otherwise multiple conflicting versions of the same crate AND even the same +numerical *version* might be built, causing *weird* errors. + +The advantage, however, is that `cargo` will pick up on file changes and rebuild +the crate on changes. + +## An in-between: system extensions + +An easy way to quickly get the new package "installed" *temporarily*, such that +real apt package upgrades are unaffected is as a system-extension. + +The easiest way — if no other extensions are used — is to just symlink the +`extensions/` directory to `/run` as root via: + +``` +# ln -s ${THIS_DIR}/extensions /run/extensions +``` + +This does not persist across reboots. +(Note: that the `extensions/` directory does not need to exist for the above to +work.) + +Once this is done, trying a new version of a crate works by: + +1. Bump the version: eg. `5.0.0+test8` -> `5.0.0+test9` + While this is technically optional (the sysext would then *replace* + (temporarily) the installed version as long as the sysext is active), just + like with `.deb` files, not doing this causes `cargo` to consider the crate + to be unchanged and it will not rebuild its code. +2. here: `$ make ${crate}-sysext` (rebuilds `extensions/${crate}.raw`) +3. as root: `# systemd-sysext refresh` (activates current extensions images) +4. in the other project: `$ cargo update && cargo build` + +In the last step, cargo sees that there's a newer version of the crate available +and use that. diff --git a/proxmox-access-control/debian/control b/proxmox-access-control/debian/control index c85cc7d1..bfe434d5 100644 --- a/proxmox-access-control/debian/control +++ b/proxmox-access-control/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-access-control Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 12), librust-proxmox-time-2+default-dev , librust-serde-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git Homepage: https://proxmox.com diff --git a/proxmox-acme-api/debian/control b/proxmox-acme-api/debian/control index 4dad5966..1cf2c24e 100644 --- a/proxmox-acme-api/debian/control +++ b/proxmox-acme-api/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-acme-api Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -17,9 +17,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: Vcs-Browser: +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-acme-api Rules-Requires-Root: no diff --git a/proxmox-acme/debian/control b/proxmox-acme/debian/control index c976bb1a..d4c6af75 100644 --- a/proxmox-acme/debian/control +++ b/proxmox-acme/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-acme Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -14,7 +14,7 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: Vcs-Browser: Homepage: https://proxmox.com @@ -69,8 +69,8 @@ Depends: librust-anyhow-1+default-dev, librust-bytes-1+default-dev, librust-hyper-0.14+default-dev (>= 0.14.5-~~), - librust-proxmox-http-0.9+client-dev (>= 0.9.2-~~), - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) + librust-proxmox-http-0.9+client-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) Provides: librust-proxmox-acme-0+async-client-dev (= ${binary:Version}), librust-proxmox-acme-0.5+async-client-dev (= ${binary:Version}), diff --git a/proxmox-api-macro/debian/control b/proxmox-api-macro/debian/control index 44106eb9..b139cc6a 100644 --- a/proxmox-api-macro/debian/control +++ b/proxmox-api-macro/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-api-macro Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -14,7 +14,7 @@ Build-Depends: debhelper (>= 12), librust-syn-2+full-dev , librust-syn-2+visit-mut-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git Homepage: https://proxmox.com diff --git a/proxmox-apt-api-types/Cargo.toml b/proxmox-apt-api-types/Cargo.toml index bc3902f1..ac56cc02 100644 --- a/proxmox-apt-api-types/Cargo.toml +++ b/proxmox-apt-api-types/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-apt-api-types" description = "APT API type definitions." -version = "1.0.1" +version = "1.0.2" authors.workspace = true edition.workspace = true diff --git a/proxmox-apt-api-types/debian/changelog b/proxmox-apt-api-types/debian/changelog index a2d7c486..1ee90525 100644 --- a/proxmox-apt-api-types/debian/changelog +++ b/proxmox-apt-api-types/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-apt-api-types (1.0.2-1) bookworm; urgency=medium + + * add Ceph Squid as valid Proxmox APT repository handle. + + -- Proxmox Support Team Sun, 10 Nov 2024 18:41:28 +0100 + rust-proxmox-apt-api-types (1.0.1-1) bookworm; urgency=medium * fix backward compatibility by encoding digest as array diff --git a/proxmox-apt-api-types/debian/control b/proxmox-apt-api-types/debian/control index 05b196ce..187a3b15 100644 --- a/proxmox-apt-api-types/debian/control +++ b/proxmox-apt-api-types/debian/control @@ -1,21 +1,22 @@ Source: rust-proxmox-apt-api-types Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , librust-proxmox-config-digest-0.1+default-dev , - librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) , - librust-proxmox-schema-3+default-dev (>= 3.1.1-~~) , + librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~) , + librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) , librust-serde-1+default-dev , librust-serde-1+derive-dev , librust-serde-plain-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox-apt.git Vcs-Browser: https://git.proxmox.com/?p=proxmox-apt.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-apt-api-types Rules-Requires-Root: no @@ -25,8 +26,8 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-proxmox-config-digest-0.1+default-dev, - librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~), - librust-proxmox-schema-3+default-dev (>= 3.1.1-~~), + librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~), + librust-proxmox-schema-3+default-dev (>= 3.1.2-~~), librust-serde-1+default-dev, librust-serde-1+derive-dev, librust-serde-plain-1+default-dev @@ -36,7 +37,7 @@ Provides: librust-proxmox-apt-api-types-1+default-dev (= ${binary:Version}), librust-proxmox-apt-api-types-1.0-dev (= ${binary:Version}), librust-proxmox-apt-api-types-1.0+default-dev (= ${binary:Version}), - librust-proxmox-apt-api-types-1.0.1-dev (= ${binary:Version}), - librust-proxmox-apt-api-types-1.0.1+default-dev (= ${binary:Version}) + librust-proxmox-apt-api-types-1.0.2-dev (= ${binary:Version}), + librust-proxmox-apt-api-types-1.0.2+default-dev (= ${binary:Version}) Description: APT API type definitions - Rust source code Source code for Debianized Rust crate "proxmox-apt-api-types" diff --git a/proxmox-apt-api-types/src/lib.rs b/proxmox-apt-api-types/src/lib.rs index 17197a15..d0ca05d9 100644 --- a/proxmox-apt-api-types/src/lib.rs +++ b/proxmox-apt-api-types/src/lib.rs @@ -386,6 +386,12 @@ pub enum APTRepositoryHandle { CephReefNoSubscription, /// Ceph Reef test repository. CephReefTest, + /// Ceph Squid enterprise repository. + CephSquidEnterprise, + /// Ceph Squid no-subscription repository. + CephSquidNoSubscription, + /// Ceph Squid test repository. + CephSquidTest, /// Check install #[serde(rename = "checkinstall")] CheckInstall, diff --git a/proxmox-apt/Cargo.toml b/proxmox-apt/Cargo.toml index 8935245f..d0c4c4b5 100644 --- a/proxmox-apt/Cargo.toml +++ b/proxmox-apt/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-apt" description = "Proxmox library for APT" -version = "0.11.3" +version = "0.11.5" exclude = ["debian"] diff --git a/proxmox-apt/debian/changelog b/proxmox-apt/debian/changelog index 8147661b..8381fd27 100644 --- a/proxmox-apt/debian/changelog +++ b/proxmox-apt/debian/changelog @@ -1,3 +1,15 @@ +rust-proxmox-apt (0.11.5-1) bookworm; urgency=medium + + * add Ceph Squid to standard repos for PVE + + -- Proxmox Support Team Mon, 11 Nov 2024 21:08:39 +0100 + +rust-proxmox-apt (0.11.4-1) bookworm; urgency=medium + + * add support for Ceph Squid repositories + + -- Proxmox Support Team Sun, 10 Nov 2024 18:48:37 +0100 + rust-proxmox-apt (0.11.3-1) bookworm; urgency=medium * drop unused dependency on once_cell diff --git a/proxmox-apt/debian/control b/proxmox-apt/debian/control index 3ae00d2a..05ac6266 100644 --- a/proxmox-apt/debian/control +++ b/proxmox-apt/debian/control @@ -1,15 +1,15 @@ Source: rust-proxmox-apt Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , librust-anyhow-1+default-dev , librust-hex-0.4+default-dev , librust-openssl-0.10+default-dev , - librust-proxmox-apt-api-types-1+default-dev (>= 1.0.1-~~) , + librust-proxmox-apt-api-types-1+default-dev (>= 1.0.2-~~) , librust-proxmox-config-digest-0.1+default-dev , librust-proxmox-config-digest-0.1+openssl-dev , librust-proxmox-sys-0.6+default-dev , @@ -18,7 +18,7 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox-apt.git Vcs-Browser: https://git.proxmox.com/?p=proxmox-apt.git Homepage: https://proxmox.com @@ -33,7 +33,7 @@ Depends: librust-anyhow-1+default-dev, librust-hex-0.4+default-dev, librust-openssl-0.10+default-dev, - librust-proxmox-apt-api-types-1+default-dev (>= 1.0.1-~~), + librust-proxmox-apt-api-types-1+default-dev (>= 1.0.2-~~), librust-proxmox-config-digest-0.1+default-dev, librust-proxmox-config-digest-0.1+openssl-dev, librust-proxmox-sys-0.6+default-dev, @@ -49,8 +49,8 @@ Provides: librust-proxmox-apt-0+default-dev (= ${binary:Version}), librust-proxmox-apt-0.11-dev (= ${binary:Version}), librust-proxmox-apt-0.11+default-dev (= ${binary:Version}), - librust-proxmox-apt-0.11.3-dev (= ${binary:Version}), - librust-proxmox-apt-0.11.3+default-dev (= ${binary:Version}) + librust-proxmox-apt-0.11.5-dev (= ${binary:Version}), + librust-proxmox-apt-0.11.5+default-dev (= ${binary:Version}) Description: Proxmox library for APT - Rust source code Source code for Debianized Rust crate "proxmox-apt" @@ -68,7 +68,7 @@ Depends: Provides: librust-proxmox-apt-0+cache-dev (= ${binary:Version}), librust-proxmox-apt-0.11+cache-dev (= ${binary:Version}), - librust-proxmox-apt-0.11.3+cache-dev (= ${binary:Version}) + librust-proxmox-apt-0.11.5+cache-dev (= ${binary:Version}) Description: Proxmox library for APT - feature "cache" This metapackage enables feature "cache" for the Rust proxmox-apt crate, by pulling in any additional dependencies needed by that feature. diff --git a/proxmox-apt/src/repositories/mod.rs b/proxmox-apt/src/repositories/mod.rs index d2d6f8d9..3d5d5b90 100644 --- a/proxmox-apt/src/repositories/mod.rs +++ b/proxmox-apt/src/repositories/mod.rs @@ -127,6 +127,9 @@ pub fn standard_repositories( APTStandardRepository::from_handle(APTRepositoryHandle::CephReefEnterprise), APTStandardRepository::from_handle(APTRepositoryHandle::CephReefNoSubscription), APTStandardRepository::from_handle(APTRepositoryHandle::CephReefTest), + APTStandardRepository::from_handle(APTRepositoryHandle::CephSquidEnterprise), + APTStandardRepository::from_handle(APTRepositoryHandle::CephSquidNoSubscription), + APTStandardRepository::from_handle(APTRepositoryHandle::CephSquidTest), ]); } } diff --git a/proxmox-apt/src/repositories/standard.rs b/proxmox-apt/src/repositories/standard.rs index 6fed4f1d..25f09eca 100644 --- a/proxmox-apt/src/repositories/standard.rs +++ b/proxmox-apt/src/repositories/standard.rs @@ -75,6 +75,17 @@ impl APTRepositoryHandleImpl for APTRepositoryHandle { "This repository contains the Ceph Reef packages before they are moved to the \ main repository." } + APTRepositoryHandle::CephSquidEnterprise => { + "This repository holds the production-ready Proxmox Ceph Squid packages." + } + APTRepositoryHandle::CephSquidNoSubscription => { + "This repository holds the Proxmox Ceph Squid packages intended for \ + non-production use." + } + APTRepositoryHandle::CephSquidTest => { + "This repository contains the Ceph Squid packages before they are moved to the \ + main repository." + } APTRepositoryHandle::CheckInstall => { "The repository contains check install information for binary \ executables and libraries." @@ -105,6 +116,9 @@ impl APTRepositoryHandleImpl for APTRepositoryHandle { APTRepositoryHandle::CephReefEnterprise => "Ceph Reef Enterprise", APTRepositoryHandle::CephReefNoSubscription => "Ceph Reef No-Subscription", APTRepositoryHandle::CephReefTest => "Ceph Reef Test", + APTRepositoryHandle::CephSquidEnterprise => "Ceph Squid Enterprise", + APTRepositoryHandle::CephSquidNoSubscription => "Ceph Squid No-Subscription", + APTRepositoryHandle::CephSquidTest => "Ceph Squid Test", APTRepositoryHandle::CheckInstall => "checkinstall", APTRepositoryHandle::Classic => "classic", APTRepositoryHandle::DebugInfo => "debuginfo", @@ -125,7 +139,10 @@ impl APTRepositoryHandleImpl for APTRepositoryHandle { | APTRepositoryHandle::CephQuincyTest | APTRepositoryHandle::CephReefEnterprise | APTRepositoryHandle::CephReefNoSubscription - | APTRepositoryHandle::CephReefTest => "/etc/apt/sources.list.d/ceph.list".to_string(), + | APTRepositoryHandle::CephReefTest + | APTRepositoryHandle::CephSquidEnterprise + | APTRepositoryHandle::CephSquidNoSubscription + | APTRepositoryHandle::CephSquidTest => "/etc/apt/sources.list.d/ceph.list".to_string(), APTRepositoryHandle::CheckInstall | APTRepositoryHandle::Classic | APTRepositoryHandle::DebugInfo @@ -198,6 +215,21 @@ impl APTRepositoryHandleImpl for APTRepositoryHandle { vec!["http://download.proxmox.com/debian/ceph-reef".to_string()], "test".to_string(), ), + APTRepositoryHandle::CephSquidEnterprise => ( + APTRepositoryPackageType::Deb, + vec!["https://enterprise.proxmox.com/debian/ceph-squid".to_string()], + "enterprise".to_string(), + ), + APTRepositoryHandle::CephSquidNoSubscription => ( + APTRepositoryPackageType::Deb, + vec!["http://download.proxmox.com/debian/ceph-squid".to_string()], + "no-subscription".to_string(), + ), + APTRepositoryHandle::CephSquidTest => ( + APTRepositoryPackageType::Deb, + vec!["http://download.proxmox.com/debian/ceph-squid".to_string()], + "test".to_string(), + ), APTRepositoryHandle::CheckInstall => ( APTRepositoryPackageType::Rpm, vec!["http://ftp.altlinux.org/pub/distributions/ALTLinux".to_string()], diff --git a/proxmox-apt/tests/repositories.rs b/proxmox-apt/tests/repositories.rs index c6883cd9..d7b403b6 100644 --- a/proxmox-apt/tests/repositories.rs +++ b/proxmox-apt/tests/repositories.rs @@ -661,6 +661,9 @@ fn test_standard_repositories() -> Result<(), Error> { APTStandardRepository::from_handle(APTRepositoryHandle::CephReefEnterprise), APTStandardRepository::from_handle(APTRepositoryHandle::CephReefNoSubscription), APTStandardRepository::from_handle(APTRepositoryHandle::CephReefTest), + APTStandardRepository::from_handle(APTRepositoryHandle::CephSquidEnterprise), + APTStandardRepository::from_handle(APTRepositoryHandle::CephSquidNoSubscription), + APTStandardRepository::from_handle(APTRepositoryHandle::CephSquidTest), ]; let absolute_suite_list = read_dir.join("absolute_suite.list"); diff --git a/proxmox-async/debian/control b/proxmox-async/debian/control index 84b21467..1159034a 100644 --- a/proxmox-async/debian/control +++ b/proxmox-async/debian/control @@ -1,10 +1,10 @@ Source: rust-proxmox-async Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native (>= 1.80) , + rustc:native , libstd-rust-dev , librust-anyhow-1+default-dev , librust-futures-0.3+default-dev , @@ -20,9 +20,10 @@ Build-Depends: debhelper (>= 12), libssl-dev , uuid-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-async Rules-Requires-Root: no diff --git a/proxmox-auth-api/debian/control b/proxmox-auth-api/debian/control index 3d63b630..68d41d32 100644 --- a/proxmox-auth-api/debian/control +++ b/proxmox-auth-api/debian/control @@ -1,16 +1,17 @@ Source: rust-proxmox-auth-api Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-auth-api Rules-Requires-Root: no diff --git a/proxmox-borrow/debian/control b/proxmox-borrow/debian/control index 2b6e7c24..42064065 100644 --- a/proxmox-borrow/debian/control +++ b/proxmox-borrow/debian/control @@ -1,15 +1,16 @@ Source: rust-proxmox-borrow Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.1 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-borrow Rules-Requires-Root: no @@ -29,5 +30,4 @@ Provides: librust-proxmox-borrow-1.0.1-dev (= ${binary:Version}), librust-proxmox-borrow-1.0.1+default-dev (= ${binary:Version}) Description: Contains the Tied type to tie a value with a lifetime to the value it borrows from - Rust source code - This package contains the source for the Rust proxmox-borrow crate, packaged by - debcargo for use with cargo and dh-cargo. + Source code for Debianized Rust crate "proxmox-borrow" diff --git a/proxmox-client/debian/control b/proxmox-client/debian/control index 930a9869..4e193403 100644 --- a/proxmox-client/debian/control +++ b/proxmox-client/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-client Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -14,9 +14,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-client Rules-Requires-Root: no @@ -55,8 +56,8 @@ Depends: librust-hyper-0.14+default-dev (>= 0.14.5-~~), librust-log-0.4+default-dev (>= 0.4.17-~~), librust-openssl-0.10+default-dev, - librust-proxmox-http-0.9+client-dev (>= 0.9.2-~~), - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) + librust-proxmox-http-0.9+client-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) Provides: librust-proxmox-client-0+hyper-client-dev (= ${binary:Version}), librust-proxmox-client-0.5+hyper-client-dev (= ${binary:Version}), diff --git a/proxmox-client/src/client.rs b/proxmox-client/src/client.rs index cf16247c..9b078a98 100644 --- a/proxmox-client/src/client.rs +++ b/proxmox-client/src/client.rs @@ -223,7 +223,10 @@ impl Client { } .map_err(|err| Error::internal("failed to build request", err))?; - let response = client.request(request).await.map_err(Error::Anyhow)?; + let response = client + .request(request) + .await + .map_err(|err| Error::Client(err.into()))?; if response.status() == StatusCode::UNAUTHORIZED { return Err(Error::Unauthorized); @@ -318,7 +321,11 @@ impl Client { .body(request.body.into()) .map_err(|err| Error::internal("error building login http request", err))?; - let api_response = self.client.request(request).await.map_err(Error::Anyhow)?; + let api_response = self + .client + .request(request) + .await + .map_err(|err| Error::Client(err.into()))?; if !api_response.status().is_success() { return Err(Error::api(api_response.status(), "authentication failed")); } diff --git a/proxmox-compression/debian/control b/proxmox-compression/debian/control index 805102d8..5c99b1fc 100644 --- a/proxmox-compression/debian/control +++ b/proxmox-compression/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-compression Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -25,9 +25,10 @@ Build-Depends: debhelper (>= 12), librust-zstd-0.12+bindgen-dev , librust-zstd-0.12+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-compression Rules-Requires-Root: no diff --git a/proxmox-config-digest/debian/control b/proxmox-config-digest/debian/control index afdbff53..be9ad3b3 100644 --- a/proxmox-config-digest/debian/control +++ b/proxmox-config-digest/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-config-digest Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-plain-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-config-digest Rules-Requires-Root: no diff --git a/proxmox-daemon/debian/control b/proxmox-daemon/debian/control index 3ebb9895..ece0c3cd 100644 --- a/proxmox-daemon/debian/control +++ b/proxmox-daemon/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-daemon Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -22,9 +22,10 @@ Build-Depends: debhelper (>= 12), librust-tokio-1+signal-dev (>= 1.6-~~) , librust-tokio-1+sync-dev (>= 1.6-~~) Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-daemon Rules-Requires-Root: no diff --git a/proxmox-dns-api/debian/control b/proxmox-dns-api/debian/control index 4d1aced8..ddbedd56 100644 --- a/proxmox-dns-api/debian/control +++ b/proxmox-dns-api/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-dns-api Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -16,9 +16,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-1+derive-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-dns-api Rules-Requires-Root: no diff --git a/proxmox-http-error/debian/control b/proxmox-http-error/debian/control index 065a12e1..5754c620 100644 --- a/proxmox-http-error/debian/control +++ b/proxmox-http-error/debian/control @@ -1,18 +1,19 @@ Source: rust-proxmox-http-error Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-http-0.2+default-dev , librust-serde-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.1 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-http-error Rules-Requires-Root: no @@ -33,5 +34,4 @@ Provides: librust-proxmox-http-error-0.1.0-dev (= ${binary:Version}), librust-proxmox-http-error-0.1.0+default-dev (= ${binary:Version}) Description: Proxmox HTTP Error - Rust source code - This package contains the source for the Rust proxmox-http-error crate, - packaged by debcargo for use with cargo and dh-cargo. + Source code for Debianized Rust crate "proxmox-http-error" diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml index bc79c53d..c8c963f7 100644 --- a/proxmox-http/Cargo.toml +++ b/proxmox-http/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-http" description = "Proxmox HTTP library" -version = "0.9.3" +version = "0.9.4" authors.workspace = true edition.workspace = true diff --git a/proxmox-http/debian/changelog b/proxmox-http/debian/changelog index d9d4a45d..eafe8572 100644 --- a/proxmox-http/debian/changelog +++ b/proxmox-http/debian/changelog @@ -1,3 +1,12 @@ +rust-proxmox-http (0.9.4-1) bookworm; urgency=medium + + * fix #5808: use native-tls instead of rustls for the sync client to avoid + problems where a IP is used as a SAN in a certificate. + + * sync client: add HTTP request timeout option. + + -- Proxmox Support Team Tue, 26 Nov 2024 14:00:00 +0100 + rust-proxmox-http (0.9.3-1) bookworm; urgency=medium * rebuild with proxmox-sys 6.0 diff --git a/proxmox-http/debian/control b/proxmox-http/debian/control index e84ae729..bd5ad8df 100644 --- a/proxmox-http/debian/control +++ b/proxmox-http/debian/control @@ -36,8 +36,8 @@ Provides: librust-proxmox-http-0+default-dev (= ${binary:Version}), librust-proxmox-http-0.9-dev (= ${binary:Version}), librust-proxmox-http-0.9+default-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+default-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4-dev (= ${binary:Version}), + librust-proxmox-http-0.9.4+default-dev (= ${binary:Version}) Description: Proxmox HTTP library - Rust source code Source code for Debianized Rust crate "proxmox-http" @@ -64,7 +64,7 @@ Depends: Provides: librust-proxmox-http-0+client-dev (= ${binary:Version}), librust-proxmox-http-0.9+client-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+client-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+client-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "client" This metapackage enables feature "client" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. @@ -83,7 +83,7 @@ Depends: Provides: librust-proxmox-http-0+client-sync-dev (= ${binary:Version}), librust-proxmox-http-0.9+client-sync-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+client-sync-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+client-sync-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "client-sync" This metapackage enables feature "client-sync" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. @@ -98,7 +98,7 @@ Depends: Provides: librust-proxmox-http-0+client-trait-dev (= ${binary:Version}), librust-proxmox-http-0.9+client-trait-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+client-trait-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+client-trait-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "client-trait" This metapackage enables feature "client-trait" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. @@ -117,7 +117,7 @@ Depends: Provides: librust-proxmox-http-0+http-helpers-dev (= ${binary:Version}), librust-proxmox-http-0.9+http-helpers-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+http-helpers-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+http-helpers-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "http-helpers" This metapackage enables feature "http-helpers" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. @@ -132,7 +132,7 @@ Depends: Provides: librust-proxmox-http-0+proxmox-async-dev (= ${binary:Version}), librust-proxmox-http-0.9+proxmox-async-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+proxmox-async-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+proxmox-async-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "proxmox-async" This metapackage enables feature "proxmox-async" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. @@ -151,7 +151,7 @@ Depends: Provides: librust-proxmox-http-0+rate-limited-stream-dev (= ${binary:Version}), librust-proxmox-http-0.9+rate-limited-stream-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+rate-limited-stream-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+rate-limited-stream-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "rate-limited-stream" This metapackage enables feature "rate-limited-stream" for the Rust proxmox- http crate, by pulling in any additional dependencies needed by that feature. @@ -166,7 +166,7 @@ Depends: Provides: librust-proxmox-http-0+rate-limiter-dev (= ${binary:Version}), librust-proxmox-http-0.9+rate-limiter-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+rate-limiter-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+rate-limiter-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "rate-limiter" This metapackage enables feature "rate-limiter" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. @@ -191,7 +191,7 @@ Depends: Provides: librust-proxmox-http-0+websocket-dev (= ${binary:Version}), librust-proxmox-http-0.9+websocket-dev (= ${binary:Version}), - librust-proxmox-http-0.9.3+websocket-dev (= ${binary:Version}) + librust-proxmox-http-0.9.4+websocket-dev (= ${binary:Version}) Description: Proxmox HTTP library - feature "websocket" This metapackage enables feature "websocket" for the Rust proxmox-http crate, by pulling in any additional dependencies needed by that feature. diff --git a/proxmox-http/src/client/sync.rs b/proxmox-http/src/client/sync.rs index fb10f5be..955a6fce 100644 --- a/proxmox-http/src/client/sync.rs +++ b/proxmox-http/src/client/sync.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::io::Read; use std::sync::Arc; +use std::time::Duration; use anyhow::Error; use http::Response; @@ -12,16 +13,31 @@ use crate::HttpOptions; /// Blocking HTTP client for usage with [`HttpClient`]. pub struct Client { options: HttpOptions, + timeout: Option, } impl Client { pub fn new(options: HttpOptions) -> Self { - Self { options } + Self { + options, + timeout: None, + } + } + + pub fn new_with_timeout(options: HttpOptions, timeout: Duration) -> Self { + Self { + options, + timeout: Some(timeout), + } } fn agent(&self) -> Result { let mut builder = ureq::AgentBuilder::new(); + if let Some(timeout) = self.timeout { + builder = builder.timeout(timeout); + }; + let connector = Arc::new(native_tls::TlsConnector::new()?); builder = builder.tls_connector(connector); diff --git a/proxmox-human-byte/debian/control b/proxmox-human-byte/debian/control index 907df5e4..697ad57d 100644 --- a/proxmox-human-byte/debian/control +++ b/proxmox-human-byte/debian/control @@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12), librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~) , librust-serde-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-human-byte Rules-Requires-Root: no diff --git a/proxmox-io/debian/control b/proxmox-io/debian/control index 9fec46e3..d32995b3 100644 --- a/proxmox-io/debian/control +++ b/proxmox-io/debian/control @@ -1,17 +1,18 @@ Source: rust-proxmox-io Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , librust-endian-trait-0.6+arrays-dev , librust-endian-trait-0.6+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-io Rules-Requires-Root: no diff --git a/proxmox-lang/debian/control b/proxmox-lang/debian/control index 64f6f6f1..1c03acd2 100644 --- a/proxmox-lang/debian/control +++ b/proxmox-lang/debian/control @@ -1,15 +1,16 @@ Source: rust-proxmox-lang Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-lang Rules-Requires-Root: no diff --git a/proxmox-ldap/debian/control b/proxmox-ldap/debian/control index f0a047de..72d251c3 100644 --- a/proxmox-ldap/debian/control +++ b/proxmox-ldap/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-ldap Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-1+derive-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-ldap Rules-Requires-Root: no diff --git a/proxmox-log/Cargo.toml b/proxmox-log/Cargo.toml index b9429542..b85e1804 100644 --- a/proxmox-log/Cargo.toml +++ b/proxmox-log/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-log" description = "Logging infrastructure for proxmox" -version = "0.2.5" +version = "0.2.7" authors.workspace = true edition.workspace = true diff --git a/proxmox-log/debian/changelog b/proxmox-log/debian/changelog index 7e4debcd..7bdbbbbb 100644 --- a/proxmox-log/debian/changelog +++ b/proxmox-log/debian/changelog @@ -1,3 +1,16 @@ +rust-proxmox-log (0.2.7-1) bookworm; urgency=medium + + * ignore to_stdout parameter, this is now handled by tracing. + + -- Proxmox Support Team Wed, 27 Nov 2024 13:29:50 +0100 + +rust-proxmox-log (0.2.6-1) bookworm; urgency=medium + + * log: only print error level to syslog/stderr to avoid that, e.g., the + whole worker task logs are mirrored to the journal. + + -- Proxmox Support Team Tue, 19 Nov 2024 11:32:00 +0100 + rust-proxmox-log (0.2.5-1) bookworm; urgency=medium * init_logger: fall back to printing to stderr when syslog is unavailable diff --git a/proxmox-log/debian/control b/proxmox-log/debian/control index abaa8f82..0e6ee26f 100644 --- a/proxmox-log/debian/control +++ b/proxmox-log/debian/control @@ -1,10 +1,10 @@ Source: rust-proxmox-log Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-nix-0.26+default-dev (>= 0.26.1-~~) , @@ -17,9 +17,10 @@ Build-Depends: debhelper (>= 12), librust-tracing-log-0.2+std-dev , librust-tracing-subscriber-0.3+default-dev (>= 0.3.16-~~) Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-log Rules-Requires-Root: no @@ -44,7 +45,7 @@ Provides: librust-proxmox-log-0+default-dev (= ${binary:Version}), librust-proxmox-log-0.2-dev (= ${binary:Version}), librust-proxmox-log-0.2+default-dev (= ${binary:Version}), - librust-proxmox-log-0.2.5-dev (= ${binary:Version}), - librust-proxmox-log-0.2.5+default-dev (= ${binary:Version}) + librust-proxmox-log-0.2.7-dev (= ${binary:Version}), + librust-proxmox-log-0.2.7+default-dev (= ${binary:Version}) Description: Logging infrastructure for proxmox - Rust source code Source code for Debianized Rust crate "proxmox-log" diff --git a/proxmox-log/src/file_logger.rs b/proxmox-log/src/file_logger.rs index c3648976..1e67b450 100644 --- a/proxmox-log/src/file_logger.rs +++ b/proxmox-log/src/file_logger.rs @@ -17,7 +17,8 @@ pub struct FileLogOptions { pub read: bool, /// If set, ensure that the file is newly created or error out if already existing. pub exclusive: bool, - /// Duplicate logged messages to STDOUT, like tee + /// Duplicate logged messages to STDOUT, like tee. + /// NOTE: this is now handled by tracing, this option will be removed soon. pub to_stdout: bool, /// Prefix messages logged to the file with the current local time as RFC 3339 pub prefix_time: bool, @@ -103,11 +104,12 @@ impl FileLogger { pub fn log>(&mut self, msg: S) { let msg = msg.as_ref(); - if self.options.to_stdout { - let mut stdout = std::io::stdout(); - let _ = stdout.write_all(msg.as_bytes()); - let _ = stdout.write_all(b"\n"); - } + // TODO: remove whole to_stdout option, handled by tracing now + //if self.options.to_stdout { + // let mut stdout = std::io::stdout(); + // let _ = stdout.write_all(msg.as_bytes()); + // let _ = stdout.write_all(b"\n"); + //} let line = if self.options.prefix_time { let now = proxmox_time::epoch_i64(); @@ -128,16 +130,18 @@ impl FileLogger { impl std::io::Write for FileLogger { fn write(&mut self, buf: &[u8]) -> Result { - if self.options.to_stdout { - let _ = std::io::stdout().write(buf); - } + // TODO: remove whole to_stdout option, handled by tracing now + //if self.options.to_stdout { + // let _ = std::io::stdout().write(buf); + //} self.file.write(buf) } fn flush(&mut self) -> Result<(), std::io::Error> { - if self.options.to_stdout { - let _ = std::io::stdout().flush(); - } + // TODO: remove whole to_stdout option, handled by tracing now + //if self.options.to_stdout { + // let _ = std::io::stdout().flush(); + //} self.file.flush() } } diff --git a/proxmox-login/debian/control b/proxmox-login/debian/control index f77d866b..addd293c 100644 --- a/proxmox-login/debian/control +++ b/proxmox-login/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-login Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/proxmox-login] Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/proxmox-login Homepage: https://proxmox.com diff --git a/proxmox-metrics/debian/control b/proxmox-metrics/debian/control index b64a4fc4..37553174 100644 --- a/proxmox-metrics/debian/control +++ b/proxmox-metrics/debian/control @@ -1,10 +1,10 @@ Source: rust-proxmox-metrics Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-futures-0.3+default-dev , @@ -12,8 +12,8 @@ Build-Depends: debhelper (>= 12), librust-hyper-0.14+default-dev (>= 0.14.5-~~) , librust-openssl-0.10+default-dev , librust-proxmox-async-0.4+default-dev (>= 0.4.1-~~) , - librust-proxmox-http-0.9+client-dev , - librust-proxmox-http-0.9+default-dev , + librust-proxmox-http-0.9+client-dev (>= 0.9.4-~~) , + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) , librust-serde-1+default-dev , librust-serde-json-1+default-dev , librust-tokio-1+default-dev (>= 1.6-~~) , @@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12), librust-tokio-1+sync-dev (>= 1.6-~~) , librust-url-2+default-dev (>= 2.2-~~) Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-metrics Rules-Requires-Root: no @@ -38,8 +39,8 @@ Depends: librust-hyper-0.14+default-dev (>= 0.14.5-~~), librust-openssl-0.10+default-dev, librust-proxmox-async-0.4+default-dev (>= 0.4.1-~~), - librust-proxmox-http-0.9+client-dev, - librust-proxmox-http-0.9+default-dev, + librust-proxmox-http-0.9+client-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~), librust-serde-1+default-dev, librust-serde-json-1+default-dev, librust-tokio-1+default-dev (>= 1.6-~~), diff --git a/proxmox-network-api/debian/control b/proxmox-network-api/debian/control index 9fa0fa31..434ce468 100644 --- a/proxmox-network-api/debian/control +++ b/proxmox-network-api/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-network-api Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -15,9 +15,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-1+derive-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-network-api Rules-Requires-Root: no diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml index d57a36cd..725bd210 100644 --- a/proxmox-notify/Cargo.toml +++ b/proxmox-notify/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-notify" description = "implementation of notification base and plugins" -version = "0.4.2" +version = "0.5.1" authors.workspace = true edition.workspace = true @@ -13,13 +13,15 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true -base64.workspace = true +base64 = { workspace = true, optional = true } const_format.workspace = true handlebars = { workspace = true } +http = { workspace = true, optional = true } lettre = { workspace = true, optional = true } log.workspace = true mail-parser = { workspace = true, optional = true } openssl.workspace = true +percent-encoding = { workspace = true, optional = true } regex.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true @@ -30,15 +32,17 @@ proxmox-human-byte.workspace = true proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] } proxmox-section-config = { workspace = true } proxmox-serde.workspace = true +proxmox-sendmail = { workspace = true, optional = true } proxmox-sys = { workspace = true, optional = true } proxmox-time.workspace = true proxmox-uuid = { workspace = true, features = ["serde"] } [features] -default = ["sendmail", "gotify", "smtp"] -mail-forwarder = ["dep:mail-parser", "dep:proxmox-sys"] -sendmail = ["dep:proxmox-sys"] +default = ["sendmail", "gotify", "smtp", "webhook"] +mail-forwarder = ["dep:mail-parser", "dep:proxmox-sys", "proxmox-sendmail/mail-forwarder"] +sendmail = ["dep:proxmox-sys", "dep:base64", "dep:proxmox-sendmail"] gotify = ["dep:proxmox-http"] pve-context = ["dep:proxmox-sys"] pbs-context = ["dep:proxmox-sys"] smtp = ["dep:lettre"] +webhook = ["dep:base64", "dep:http", "dep:percent-encoding", "dep:proxmox-http"] diff --git a/proxmox-notify/debian/changelog b/proxmox-notify/debian/changelog index 8f5c9cda..65bcd8a0 100644 --- a/proxmox-notify/debian/changelog +++ b/proxmox-notify/debian/changelog @@ -1,3 +1,23 @@ +rust-proxmox-notify (0.5.1-1) bookworm; urgency=medium + + * sendmail: various cleanups and refactoring + + * sendmail: always send multi-part message to improve rendering in certain + mail clients. + + * remove irritating 'html template not found' log message for now. + + * gotify, webhooks: timeout requests after 10 seconds to avoid blocking any + API thread/worker-process for too long. + + -- Proxmox Support Team Tue, 26 Nov 2024 14:19:09 +0100 + +rust-proxmox-notify (0.5.0-1) bookworm; urgency=medium + + * implement webhook targets and api to manage them + + -- Proxmox Support Team Sun, 10 Nov 2024 18:57:36 +0100 + rust-proxmox-notify (0.4.2-1) bookworm; urgency=medium * upgrade to proxmox-sys 6.0 diff --git a/proxmox-notify/debian/control b/proxmox-notify/debian/control index 54aa4f85..d4b6fcd1 100644 --- a/proxmox-notify/debian/control +++ b/proxmox-notify/debian/control @@ -1,26 +1,28 @@ Source: rust-proxmox-notify Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-base64-0.13+default-dev , librust-const-format-0.2+default-dev , librust-handlebars-3+default-dev , + librust-http-0.2+default-dev , librust-lettre-0.11+default-dev (>= 0.11.1-~~) , librust-log-0.4+default-dev (>= 0.4.17-~~) , librust-openssl-0.10+default-dev , - librust-proxmox-http-0.9+client-sync-dev (>= 0.9.2-~~) , - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) , + librust-percent-encoding-2+default-dev (>= 2.1-~~) , + librust-proxmox-http-0.9+client-sync-dev (>= 0.9.4-~~) , + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) , librust-proxmox-http-error-0.1+default-dev , librust-proxmox-human-byte-0.1+default-dev , - librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) , - librust-proxmox-schema-3+api-types-dev (>= 3.1.1-~~) , - librust-proxmox-schema-3+default-dev (>= 3.1.1-~~) , - librust-proxmox-section-config-2+default-dev , + librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~) , + librust-proxmox-schema-3+api-types-dev (>= 3.1.2-~~) , + librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) , + librust-proxmox-section-config-2+default-dev (>= 2.1.0-~~) , librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~) , librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~) , librust-proxmox-sys-0.6+default-dev , @@ -32,9 +34,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-notify Rules-Requires-Root: no @@ -44,17 +47,16 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-anyhow-1+default-dev, - librust-base64-0.13+default-dev, librust-const-format-0.2+default-dev, librust-handlebars-3+default-dev, librust-log-0.4+default-dev (>= 0.4.17-~~), librust-openssl-0.10+default-dev, librust-proxmox-http-error-0.1+default-dev, librust-proxmox-human-byte-0.1+default-dev, - librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~), - librust-proxmox-schema-3+api-types-dev (>= 3.1.1-~~), - librust-proxmox-schema-3+default-dev (>= 3.1.1-~~), - librust-proxmox-section-config-2+default-dev, + librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~), + librust-proxmox-schema-3+api-types-dev (>= 3.1.2-~~), + librust-proxmox-schema-3+default-dev (>= 3.1.2-~~), + librust-proxmox-section-config-2+default-dev (>= 2.1.0-~~), librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~), librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~), librust-proxmox-time-2+default-dev, @@ -70,12 +72,14 @@ Suggests: librust-proxmox-notify+gotify-dev (= ${binary:Version}), librust-proxmox-notify+mail-forwarder-dev (= ${binary:Version}), librust-proxmox-notify+pbs-context-dev (= ${binary:Version}), - librust-proxmox-notify+smtp-dev (= ${binary:Version}) + librust-proxmox-notify+sendmail-dev (= ${binary:Version}), + librust-proxmox-notify+smtp-dev (= ${binary:Version}), + librust-proxmox-notify+webhook-dev (= ${binary:Version}) Provides: librust-proxmox-notify-0-dev (= ${binary:Version}), - librust-proxmox-notify-0.4-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2-dev (= ${binary:Version}) -Description: Rust crate "proxmox-notify" - Rust source code + librust-proxmox-notify-0.5-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1-dev (= ${binary:Version}) +Description: Notification base and plugins - Rust source code Source code for Debianized Rust crate "proxmox-notify" Package: librust-proxmox-notify+default-dev @@ -86,12 +90,13 @@ Depends: librust-proxmox-notify-dev (= ${binary:Version}), librust-proxmox-notify+sendmail-dev (= ${binary:Version}), librust-proxmox-notify+gotify-dev (= ${binary:Version}), - librust-proxmox-notify+smtp-dev (= ${binary:Version}) + librust-proxmox-notify+smtp-dev (= ${binary:Version}), + librust-proxmox-notify+webhook-dev (= ${binary:Version}) Provides: librust-proxmox-notify-0+default-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+default-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+default-dev (= ${binary:Version}) -Description: Rust crate "proxmox-notify" - feature "default" + librust-proxmox-notify-0.5+default-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+default-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "default" This metapackage enables feature "default" for the Rust proxmox-notify crate, by pulling in any additional dependencies needed by that feature. @@ -101,13 +106,13 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-proxmox-notify-dev (= ${binary:Version}), - librust-proxmox-http-0.9+client-sync-dev (>= 0.9.2-~~), - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) + librust-proxmox-http-0.9+client-sync-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) Provides: librust-proxmox-notify-0+gotify-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+gotify-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+gotify-dev (= ${binary:Version}) -Description: Rust crate "proxmox-notify" - feature "gotify" + librust-proxmox-notify-0.5+gotify-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+gotify-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "gotify" This metapackage enables feature "gotify" for the Rust proxmox-notify crate, by pulling in any additional dependencies needed by that feature. @@ -121,9 +126,9 @@ Depends: librust-proxmox-sys-0.6+default-dev Provides: librust-proxmox-notify-0+mail-forwarder-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+mail-forwarder-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+mail-forwarder-dev (= ${binary:Version}) -Description: Rust crate "proxmox-notify" - feature "mail-forwarder" + librust-proxmox-notify-0.5+mail-forwarder-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+mail-forwarder-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "mail-forwarder" This metapackage enables feature "mail-forwarder" for the Rust proxmox-notify crate, by pulling in any additional dependencies needed by that feature. @@ -136,22 +141,33 @@ Depends: librust-proxmox-sys-0.6+default-dev Provides: librust-proxmox-notify+pve-context-dev (= ${binary:Version}), - librust-proxmox-notify+sendmail-dev (= ${binary:Version}), librust-proxmox-notify-0+pbs-context-dev (= ${binary:Version}), librust-proxmox-notify-0+pve-context-dev (= ${binary:Version}), - librust-proxmox-notify-0+sendmail-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+pbs-context-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+pve-context-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+sendmail-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+pbs-context-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+pve-context-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+sendmail-dev (= ${binary:Version}) -Description: Rust crate "proxmox-notify" - feature "pbs-context" and 2 more + librust-proxmox-notify-0.5+pbs-context-dev (= ${binary:Version}), + librust-proxmox-notify-0.5+pve-context-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+pbs-context-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+pve-context-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "pbs-context" and 1 more This metapackage enables feature "pbs-context" for the Rust proxmox-notify crate, by pulling in any additional dependencies needed by that feature. . - Additionally, this package also provides the "pve-context", and "sendmail" - features. + Additionally, this package also provides the "pve-context" feature. + +Package: librust-proxmox-notify+sendmail-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-notify-dev (= ${binary:Version}), + librust-base64-0.13+default-dev, + librust-proxmox-sys-0.6+default-dev +Provides: + librust-proxmox-notify-0+sendmail-dev (= ${binary:Version}), + librust-proxmox-notify-0.5+sendmail-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+sendmail-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "sendmail" + This metapackage enables feature "sendmail" for the Rust proxmox-notify crate, + by pulling in any additional dependencies needed by that feature. Package: librust-proxmox-notify+smtp-dev Architecture: any @@ -162,8 +178,27 @@ Depends: librust-lettre-0.11+default-dev (>= 0.11.1-~~) Provides: librust-proxmox-notify-0+smtp-dev (= ${binary:Version}), - librust-proxmox-notify-0.4+smtp-dev (= ${binary:Version}), - librust-proxmox-notify-0.4.2+smtp-dev (= ${binary:Version}) -Description: Rust crate "proxmox-notify" - feature "smtp" + librust-proxmox-notify-0.5+smtp-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+smtp-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "smtp" This metapackage enables feature "smtp" for the Rust proxmox-notify crate, by pulling in any additional dependencies needed by that feature. + +Package: librust-proxmox-notify+webhook-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-notify-dev (= ${binary:Version}), + librust-base64-0.13+default-dev, + librust-http-0.2+default-dev, + librust-percent-encoding-2+default-dev (>= 2.1-~~), + librust-proxmox-http-0.9+client-sync-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) +Provides: + librust-proxmox-notify-0+webhook-dev (= ${binary:Version}), + librust-proxmox-notify-0.5+webhook-dev (= ${binary:Version}), + librust-proxmox-notify-0.5.1+webhook-dev (= ${binary:Version}) +Description: Notification base and plugins - feature "webhook" + This metapackage enables feature "webhook" for the Rust proxmox-notify crate, + by pulling in any additional dependencies needed by that feature. diff --git a/proxmox-notify/src/api/mod.rs b/proxmox-notify/src/api/mod.rs index a7f6261c..7f823bc7 100644 --- a/proxmox-notify/src/api/mod.rs +++ b/proxmox-notify/src/api/mod.rs @@ -15,6 +15,8 @@ pub mod matcher; pub mod sendmail; #[cfg(feature = "smtp")] pub mod smtp; +#[cfg(feature = "webhook")] +pub mod webhook; // We have our own, local versions of http_err and http_bail, because // we don't want to wrap the error in anyhow::Error. If we were to do that, @@ -54,6 +56,9 @@ pub enum EndpointType { /// Gotify endpoint #[cfg(feature = "gotify")] Gotify, + /// Webhook endpoint + #[cfg(feature = "webhook")] + Webhook, } #[api] @@ -113,6 +118,17 @@ pub fn get_targets(config: &Config) -> Result, HttpError> { }) } + #[cfg(feature = "webhook")] + for endpoint in webhook::get_endpoints(config)? { + targets.push(Target { + name: endpoint.name, + origin: endpoint.origin.unwrap_or(Origin::UserCreated), + endpoint_type: EndpointType::Webhook, + disable: endpoint.disable, + comment: endpoint.comment, + }) + } + Ok(targets) } @@ -145,6 +161,10 @@ fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Resul { exists = exists || smtp::get_endpoint(config, name).is_ok(); } + #[cfg(feature = "webhook")] + { + exists = exists || webhook::get_endpoint(config, name).is_ok(); + } if !exists { http_bail!(NOT_FOUND, "endpoint '{name}' does not exist") diff --git a/proxmox-notify/src/api/webhook.rs b/proxmox-notify/src/api/webhook.rs new file mode 100644 index 00000000..f786c36b --- /dev/null +++ b/proxmox-notify/src/api/webhook.rs @@ -0,0 +1,432 @@ +//! CRUD API for webhook targets. +//! +//! All methods assume that the caller has already done any required permission checks. + +use proxmox_http_error::HttpError; +use proxmox_schema::property_string::PropertyString; + +use crate::api::http_err; +use crate::endpoints::webhook::{ + DeleteableWebhookProperty, KeyAndBase64Val, WebhookConfig, WebhookConfigUpdater, + WebhookPrivateConfig, WEBHOOK_TYPENAME, +}; +use crate::{http_bail, Config}; + +use super::remove_private_config_entry; +use super::set_private_config_entry; + +/// Get a list of all webhook endpoints. +/// +/// The caller is responsible for any needed permission checks. +/// Returns a list of all webhook endpoints or a [`HttpError`] if the config is +/// erroneous (`500 Internal server error`). +pub fn get_endpoints(config: &Config) -> Result, HttpError> { + let mut endpoints: Vec = config + .config + .convert_to_typed_array(WEBHOOK_TYPENAME) + .map_err(|e| http_err!(NOT_FOUND, "Could not fetch endpoints: {e}"))?; + + for endpoint in &mut endpoints { + let priv_config: WebhookPrivateConfig = config + .private_config + .lookup(WEBHOOK_TYPENAME, &endpoint.name) + .unwrap_or_default(); + + let mut secret_names = Vec::new(); + // We only return *which* secrets we have stored, but not their values. + for secret in priv_config.secret { + secret_names.push( + KeyAndBase64Val { + name: secret.name.clone(), + value: None, + } + .into(), + ) + } + + endpoint.secret = secret_names; + } + + Ok(endpoints) +} + +/// Get webhook endpoint with given `name` +/// +/// The caller is responsible for any needed permission checks. +/// Returns the endpoint or a [`HttpError`] if the endpoint was not found (`404 Not found`). +pub fn get_endpoint(config: &Config, name: &str) -> Result { + let mut endpoint: WebhookConfig = config + .config + .lookup(WEBHOOK_TYPENAME, name) + .map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))?; + + let priv_config: Option = config + .private_config + .lookup(WEBHOOK_TYPENAME, &endpoint.name) + .ok(); + + let mut secret_names = Vec::new(); + if let Some(priv_config) = priv_config { + for secret in &priv_config.secret { + secret_names.push( + KeyAndBase64Val { + name: secret.name.clone(), + value: None, + } + .into(), + ); + } + } + + endpoint.secret = secret_names; + + Ok(endpoint) +} + +/// Add a new webhook endpoint. +/// +/// The caller is responsible for any needed permission checks. +/// The caller also responsible for locking the configuration files. +/// Returns a [`HttpError`] if: +/// - the target name is already used (`400 Bad request`) +/// - an entity with the same name already exists (`400 Bad request`) +/// - the configuration could not be saved (`500 Internal server error`) +pub fn add_endpoint( + config: &mut Config, + mut endpoint_config: WebhookConfig, +) -> Result<(), HttpError> { + super::ensure_unique(config, &endpoint_config.name)?; + + let secrets = std::mem::take(&mut endpoint_config.secret); + + set_private_config_entry( + config, + &WebhookPrivateConfig { + name: endpoint_config.name.clone(), + secret: secrets, + }, + WEBHOOK_TYPENAME, + &endpoint_config.name, + )?; + + config + .config + .set_data(&endpoint_config.name, WEBHOOK_TYPENAME, &endpoint_config) + .map_err(|e| { + http_err!( + INTERNAL_SERVER_ERROR, + "could not save endpoint '{}': {e}", + endpoint_config.name + ) + }) +} + +/// Update existing webhook endpoint. +/// +/// The caller is responsible for any needed permission checks. +/// The caller also responsible for locking the configuration files. +/// Returns a `HttpError` if: +/// - the passed `digest` does not match (`400 Bad request`) +/// - parameters are ill-formed (empty header value, invalid base64, unknown header/secret) +/// (`400 Bad request`) +/// - an entity with the same name already exists (`400 Bad request`) +/// - the configuration could not be saved (`500 Internal server error`) +pub fn update_endpoint( + config: &mut Config, + name: &str, + config_updater: WebhookConfigUpdater, + delete: Option<&[DeleteableWebhookProperty]>, + digest: Option<&[u8]>, +) -> Result<(), HttpError> { + super::verify_digest(config, digest)?; + + let mut endpoint = get_endpoint(config, name)?; + endpoint.secret.clear(); + + let old_secrets = config + .private_config + .lookup::(WEBHOOK_TYPENAME, name) + .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, "could not read secret config: {err}"))? + .secret; + + if let Some(delete) = delete { + for deleteable_property in delete { + match deleteable_property { + DeleteableWebhookProperty::Comment => endpoint.comment = None, + DeleteableWebhookProperty::Disable => endpoint.disable = None, + DeleteableWebhookProperty::Header => endpoint.header = Vec::new(), + DeleteableWebhookProperty::Body => endpoint.body = None, + DeleteableWebhookProperty::Secret => { + set_private_config_entry( + config, + &WebhookPrivateConfig { + name: name.into(), + secret: Vec::new(), + }, + WEBHOOK_TYPENAME, + name, + )?; + } + } + } + } + + // Destructuring makes sure we don't forget any members + let WebhookConfigUpdater { + url, + body, + header, + method, + disable, + comment, + secret, + } = config_updater; + + if let Some(url) = url { + endpoint.url = url; + } + + if let Some(body) = body { + endpoint.body = Some(body); + } + + if let Some(header) = header { + for h in &header { + if h.value.is_none() { + http_bail!(BAD_REQUEST, "header '{}' has empty value", h.name); + } + if h.decode_value().is_err() { + http_bail!( + BAD_REQUEST, + "header '{}' does not have valid base64 encoded data", + h.name + ) + } + } + endpoint.header = header; + } + + if let Some(method) = method { + endpoint.method = method; + } + + if let Some(disable) = disable { + endpoint.disable = Some(disable); + } + + if let Some(comment) = comment { + endpoint.comment = Some(comment); + } + + if let Some(secret) = secret { + let mut new_secrets: Vec> = Vec::new(); + + for new_secret in &secret { + let sec = if new_secret.value.is_some() { + // Updating or creating a secret + + // Make sure it is valid base64 encoded data + if new_secret.decode_value().is_err() { + http_bail!( + BAD_REQUEST, + "secret '{}' does not have valid base64 encoded data", + new_secret.name + ) + } + new_secret.clone() + } else if let Some(old_secret) = old_secrets.iter().find(|v| v.name == new_secret.name) + { + // Keeping an already existing secret + old_secret.clone() + } else { + http_bail!(BAD_REQUEST, "secret '{}' not known", new_secret.name); + }; + + if new_secrets.iter().any(|s| sec.name == s.name) { + http_bail!(BAD_REQUEST, "secret '{}' defined multiple times", sec.name) + } + + new_secrets.push(sec); + } + + set_private_config_entry( + config, + &WebhookPrivateConfig { + name: name.into(), + secret: new_secrets, + }, + WEBHOOK_TYPENAME, + name, + )?; + } + + config + .config + .set_data(name, WEBHOOK_TYPENAME, &endpoint) + .map_err(|e| { + http_err!( + INTERNAL_SERVER_ERROR, + "could not save endpoint '{name}': {e}" + ) + }) +} + +/// Delete existing webhook endpoint. +/// +/// The caller is responsible for any needed permission checks. +/// The caller also responsible for locking the configuration files. +/// Returns a `HttpError` if: +/// - the entity does not exist (`404 Not found`) +/// - the endpoint is still referenced by another entity (`400 Bad request`) +pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError> { + // Check if the endpoint exists + let _ = get_endpoint(config, name)?; + super::ensure_safe_to_delete(config, name)?; + + remove_private_config_entry(config, name)?; + config.config.sections.remove(name); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{api::test_helpers::empty_config, endpoints::webhook::HttpMethod}; + + use base64::encode; + + pub fn add_default_webhook_endpoint(config: &mut Config) -> Result<(), HttpError> { + add_endpoint( + config, + WebhookConfig { + name: "webhook-endpoint".into(), + method: HttpMethod::Post, + url: "http://example.com/webhook".into(), + header: vec![KeyAndBase64Val::new_with_plain_value( + "Content-Type", + "application/json", + ) + .into()], + body: Some(encode("this is the body")), + comment: Some("comment".into()), + disable: Some(false), + secret: vec![KeyAndBase64Val::new_with_plain_value("token", "secret").into()], + ..Default::default() + }, + )?; + + assert!(get_endpoint(config, "webhook-endpoint").is_ok()); + Ok(()) + } + + #[test] + fn test_update_not_existing_returns_error() -> Result<(), HttpError> { + let mut config = empty_config(); + + assert!(update_endpoint(&mut config, "test", Default::default(), None, None).is_err()); + + Ok(()) + } + + #[test] + fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> { + let mut config = empty_config(); + add_default_webhook_endpoint(&mut config)?; + + assert!(update_endpoint( + &mut config, + "webhook-endpoint", + Default::default(), + None, + Some(&[0; 32]) + ) + .is_err()); + + Ok(()) + } + + #[test] + fn test_update() -> Result<(), HttpError> { + let mut config = empty_config(); + add_default_webhook_endpoint(&mut config)?; + + let digest = config.digest; + + update_endpoint( + &mut config, + "webhook-endpoint", + WebhookConfigUpdater { + url: Some("http://new.example.com/webhook".into()), + comment: Some("newcomment".into()), + method: Some(HttpMethod::Put), + // Keep the old token and set a new one + secret: Some(vec![ + KeyAndBase64Val::new_with_plain_value("token2", "newsecret").into(), + KeyAndBase64Val { + name: "token".into(), + value: None, + } + .into(), + ]), + ..Default::default() + }, + None, + Some(&digest), + )?; + + let endpoint = get_endpoint(&config, "webhook-endpoint")?; + + assert_eq!(endpoint.url, "http://new.example.com/webhook".to_string()); + assert_eq!(endpoint.comment, Some("newcomment".to_string())); + assert!(matches!(endpoint.method, HttpMethod::Put)); + + let secrets = config + .private_config + .lookup::(WEBHOOK_TYPENAME, "webhook-endpoint") + .unwrap() + .secret; + + assert_eq!(secrets[1].name, "token".to_string()); + assert_eq!(secrets[1].value, Some(encode("secret"))); + assert_eq!(secrets[0].name, "token2".to_string()); + assert_eq!(secrets[0].value, Some(encode("newsecret"))); + + // Test property deletion + update_endpoint( + &mut config, + "webhook-endpoint", + Default::default(), + Some(&[ + DeleteableWebhookProperty::Comment, + DeleteableWebhookProperty::Secret, + ]), + None, + )?; + + let endpoint = get_endpoint(&config, "webhook-endpoint")?; + assert_eq!(endpoint.comment, None); + + let secrets = config + .private_config + .lookup::(WEBHOOK_TYPENAME, "webhook-endpoint") + .unwrap() + .secret; + + assert!(secrets.is_empty()); + + Ok(()) + } + + #[test] + fn test_delete() -> Result<(), HttpError> { + let mut config = empty_config(); + add_default_webhook_endpoint(&mut config)?; + + delete_endpoint(&mut config, "webhook-endpoint")?; + assert!(delete_endpoint(&mut config, "webhook-endpoint").is_err()); + assert_eq!(get_endpoints(&config)?.len(), 0); + + Ok(()) + } +} diff --git a/proxmox-notify/src/config.rs b/proxmox-notify/src/config.rs index 789c4a7d..4d0b53f7 100644 --- a/proxmox-notify/src/config.rs +++ b/proxmox-notify/src/config.rs @@ -57,6 +57,17 @@ fn config_init() -> SectionConfig { GOTIFY_SCHEMA, )); } + #[cfg(feature = "webhook")] + { + use crate::endpoints::webhook::{WebhookConfig, WEBHOOK_TYPENAME}; + + const WEBHOOK_SCHEMA: &ObjectSchema = WebhookConfig::API_SCHEMA.unwrap_object_schema(); + config.register_plugin(SectionConfigPlugin::new( + WEBHOOK_TYPENAME.to_string(), + Some(String::from("name")), + WEBHOOK_SCHEMA, + )); + } const MATCHER_SCHEMA: &ObjectSchema = MatcherConfig::API_SCHEMA.unwrap_object_schema(); config.register_plugin(SectionConfigPlugin::new( @@ -110,6 +121,18 @@ fn private_config_init() -> SectionConfig { )); } + #[cfg(feature = "webhook")] + { + use crate::endpoints::webhook::{WebhookPrivateConfig, WEBHOOK_TYPENAME}; + + const WEBHOOK_SCHEMA: &ObjectSchema = + WebhookPrivateConfig::API_SCHEMA.unwrap_object_schema(); + config.register_plugin(SectionConfigPlugin::new( + WEBHOOK_TYPENAME.to_string(), + Some(String::from("name")), + WEBHOOK_SCHEMA, + )); + } config } diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs index fae036cd..3e977131 100644 --- a/proxmox-notify/src/endpoints/gotify.rs +++ b/proxmox-notify/src/endpoints/gotify.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::time::Duration; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -13,6 +14,8 @@ use crate::renderer::TemplateType; use crate::schema::ENTITY_NAME_SCHEMA; use crate::{renderer, Content, Endpoint, Error, Notification, Origin, Severity}; +const HTTP_TIMEOUT: Duration = Duration::from_secs(10); + fn severity_to_priority(level: Severity) -> u32 { match level { Severity::Info => 1, @@ -146,7 +149,7 @@ impl Endpoint for GotifyEndpoint { ..Default::default() }; - let client = Client::new(options); + let client = Client::new_with_timeout(options, HTTP_TIMEOUT); let uri = format!("{}/message", self.config.server); client diff --git a/proxmox-notify/src/endpoints/mod.rs b/proxmox-notify/src/endpoints/mod.rs index 97f79fcc..f20bee21 100644 --- a/proxmox-notify/src/endpoints/mod.rs +++ b/proxmox-notify/src/endpoints/mod.rs @@ -4,5 +4,7 @@ pub mod gotify; pub mod sendmail; #[cfg(feature = "smtp")] pub mod smtp; +#[cfg(feature = "webhook")] +pub mod webhook; mod common; diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs index 45e927ba..70b0f111 100644 --- a/proxmox-notify/src/endpoints/sendmail.rs +++ b/proxmox-notify/src/endpoints/sendmail.rs @@ -1,6 +1,4 @@ -use std::io::Write; -use std::process::{Command, Stdio}; - +use proxmox_sendmail::Mail; use serde::{Deserialize, Serialize}; use proxmox_schema::api_types::COMMENT_SCHEMA; @@ -137,19 +135,18 @@ impl Endpoint for SendmailEndpoint { .clone() .unwrap_or_else(|| context().default_sendmail_author()); - sendmail( - &recipients_str, - &subject, - Some(&text_part), - Some(&html_part), - Some(&mailfrom), - Some(&author), - ) - .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into())) + let mut mail = Mail::new(&author, &mailfrom, &subject, &text_part) + .with_html_alt(&html_part) + .with_unmasked_recipients(); + + recipients_str.iter().for_each(|r| mail.add_recipient(r)); + + mail.send() + .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into())) } #[cfg(feature = "mail-forwarder")] Content::ForwardedMail { raw, uid, .. } => { - forward(&recipients_str, &mailfrom, raw, *uid) + Mail::forward(&recipients_str, &mailfrom, raw, *uid) .map_err(|err| Error::NotifyFailed(self.config.name.clone(), err.into())) } } @@ -164,185 +161,3 @@ impl Endpoint for SendmailEndpoint { self.config.disable.unwrap_or_default() } } - -/// Sends multi-part mail with text and/or html to a list of recipients -/// -/// Includes the header `Auto-Submitted: auto-generated`, so that auto-replies -/// (i.e. OOO replies) won't trigger. -/// ``sendmail`` is used for sending the mail. -fn sendmail( - mailto: &[&str], - subject: &str, - text: Option<&str>, - html: Option<&str>, - mailfrom: Option<&str>, - author: Option<&str>, -) -> Result<(), Error> { - use std::fmt::Write as _; - - if mailto.is_empty() { - return Err(Error::Generic( - "At least one recipient has to be specified!".into(), - )); - } - let mailfrom = mailfrom.unwrap_or("root"); - let recipients = mailto.join(","); - let author = author.unwrap_or("Proxmox Backup Server"); - - let now = proxmox_time::epoch_i64(); - - let mut sendmail_process = match Command::new("/usr/sbin/sendmail") - .arg("-B") - .arg("8BITMIME") - .arg("-f") - .arg(mailfrom) - .arg("--") - .args(mailto) - .stdin(Stdio::piped()) - .spawn() - { - Err(err) => { - return Err(Error::Generic(format!( - "could not spawn sendmail process: {err}" - ))) - } - Ok(process) => process, - }; - let mut is_multipart = false; - if let (Some(_), Some(_)) = (text, html) { - is_multipart = true; - } - - let mut body = String::new(); - let boundary = format!("----_=_NextPart_001_{}", now); - if is_multipart { - body.push_str("Content-Type: multipart/alternative;\n"); - let _ = writeln!(body, "\tboundary=\"{}\"", boundary); - body.push_str("MIME-Version: 1.0\n"); - } else if !subject.is_ascii() { - body.push_str("MIME-Version: 1.0\n"); - } - if !subject.is_ascii() { - let _ = writeln!(body, "Subject: =?utf-8?B?{}?=", base64::encode(subject)); - } else { - let _ = writeln!(body, "Subject: {}", subject); - } - let _ = writeln!(body, "From: {} <{}>", author, mailfrom); - let _ = writeln!(body, "To: {}", &recipients); - let rfc2822_date = proxmox_time::epoch_to_rfc2822(now) - .map_err(|err| Error::Generic(format!("failed to format time: {err}")))?; - let _ = writeln!(body, "Date: {}", rfc2822_date); - body.push_str("Auto-Submitted: auto-generated;\n"); - - if is_multipart { - body.push('\n'); - body.push_str("This is a multi-part message in MIME format.\n"); - let _ = write!(body, "\n--{}\n", boundary); - } - if let Some(text) = text { - body.push_str("Content-Type: text/plain;\n"); - body.push_str("\tcharset=\"UTF-8\"\n"); - body.push_str("Content-Transfer-Encoding: 8bit\n"); - body.push('\n'); - body.push_str(text); - if is_multipart { - let _ = write!(body, "\n--{}\n", boundary); - } - } - if let Some(html) = html { - body.push_str("Content-Type: text/html;\n"); - body.push_str("\tcharset=\"UTF-8\"\n"); - body.push_str("Content-Transfer-Encoding: 8bit\n"); - body.push('\n'); - body.push_str(html); - if is_multipart { - let _ = write!(body, "\n--{}--", boundary); - } - } - - if let Err(err) = sendmail_process - .stdin - .take() - .unwrap() - .write_all(body.as_bytes()) - { - return Err(Error::Generic(format!( - "couldn't write to sendmail stdin: {err}" - ))); - }; - - // wait() closes stdin of the child - if let Err(err) = sendmail_process.wait() { - return Err(Error::Generic(format!( - "sendmail did not exit successfully: {err}" - ))); - } - - Ok(()) -} - -/// Forwards an email message to a given list of recipients. -/// -/// ``sendmail`` is used for sending the mail, thus `message` must be -/// compatible with that (the message is piped into stdin unmodified). -#[cfg(feature = "mail-forwarder")] -fn forward(mailto: &[&str], mailfrom: &str, message: &[u8], uid: Option) -> Result<(), Error> { - use std::os::unix::process::CommandExt; - - if mailto.is_empty() { - return Err(Error::Generic( - "At least one recipient has to be specified!".into(), - )); - } - - let mut builder = Command::new("/usr/sbin/sendmail"); - - builder - .args([ - "-N", "never", // never send DSN (avoid mail loops) - "-f", mailfrom, "--", - ]) - .args(mailto) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - if let Some(uid) = uid { - builder.uid(uid); - } - - let mut process = builder - .spawn() - .map_err(|err| Error::Generic(format!("could not spawn sendmail process: {err}")))?; - - process - .stdin - .take() - .unwrap() - .write_all(message) - .map_err(|err| Error::Generic(format!("couldn't write to sendmail stdin: {err}")))?; - - process - .wait() - .map_err(|err| Error::Generic(format!("sendmail did not exit successfully: {err}")))?; - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn email_without_recipients() { - let result = sendmail( - &[], - "Subject2", - None, - Some("HTML"), - None, - Some("test1"), - ); - assert!(result.is_err()); - } -} diff --git a/proxmox-notify/src/endpoints/webhook.rs b/proxmox-notify/src/endpoints/webhook.rs new file mode 100644 index 00000000..34dbac54 --- /dev/null +++ b/proxmox-notify/src/endpoints/webhook.rs @@ -0,0 +1,554 @@ +//! This endpoint implements a generic webhook target, allowing users to send notifications through +//! a highly customizable HTTP request. +//! +//! The configuration options include specifying the HTTP method, URL, headers, and body. +//! URLs, headers, and the body support template expansion using the [`handlebars`] templating engine. +//! For secure handling of passwords or tokens, these values can be stored as secrets. +//! Secrets are kept in a private configuration file, accessible only by root, and are not retrievable via the API. +//! Within templates, secrets can be referenced using `{{ secrets. }}`. +//! Additionally, we take measures to prevent secrets from appearing in logs or error messages. +use std::time::Duration; + +use handlebars::{ + Context as HandlebarsContext, Handlebars, Helper, HelperResult, Output, RenderContext, + RenderError as HandlebarsRenderError, +}; +use http::Request; +use percent_encoding::AsciiSet; +use proxmox_schema::property_string::PropertyString; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; + +use proxmox_http::client::sync::Client; +use proxmox_http::{HttpClient, HttpOptions, ProxyConfig}; +use proxmox_schema::api_types::{COMMENT_SCHEMA, HTTP_URL_SCHEMA}; +use proxmox_schema::{api, ApiStringFormat, ApiType, Schema, StringSchema, Updater}; + +use crate::context::context; +use crate::renderer::TemplateType; +use crate::schema::ENTITY_NAME_SCHEMA; +use crate::{renderer, Content, Endpoint, Error, Notification, Origin}; + +/// This will be used as a section type in the public/private configuration file. +pub(crate) const WEBHOOK_TYPENAME: &str = "webhook"; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(10); + +#[api] +#[derive(Serialize, Deserialize, Clone, Copy, Default)] +#[serde(rename_all = "kebab-case")] +/// HTTP Method to use. +pub enum HttpMethod { + /// HTTP POST + #[default] + Post, + /// HTTP PUT + Put, + /// HTTP GET + Get, +} + +// We only ever need a &str, so we rather implement this +// instead of Display. +impl From for &str { + fn from(value: HttpMethod) -> Self { + match value { + HttpMethod::Post => "POST", + HttpMethod::Put => "PUT", + HttpMethod::Get => "GET", + } + } +} + +#[api( + properties: { + name: { + schema: ENTITY_NAME_SCHEMA, + }, + url: { + schema: HTTP_URL_SCHEMA, + }, + comment: { + optional: true, + schema: COMMENT_SCHEMA, + }, + header: { + type: Array, + items: { + schema: KEY_AND_BASE64_VALUE_SCHEMA, + }, + optional: true, + }, + secret: { + type: Array, + items: { + schema: KEY_AND_BASE64_VALUE_SCHEMA, + }, + optional: true, + }, + } +)] +#[derive(Serialize, Deserialize, Updater, Default, Clone)] +#[serde(rename_all = "kebab-case")] +/// Config for Webhook notification endpoints +pub struct WebhookConfig { + /// Name of the endpoint. + #[updater(skip)] + pub name: String, + + pub method: HttpMethod, + + /// Webhook URL. Supports templating. + pub url: String, + /// Array of HTTP headers. Each entry is a property string with a name and a value. + /// The value property contains the header in base64 encoding. Supports templating. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub header: Vec>, + /// The HTTP body to send. Supports templating. + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + + /// Comment. + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + /// Disable this target. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable: Option, + /// Origin of this config entry. + #[serde(skip_serializing_if = "Option::is_none")] + #[updater(skip)] + pub origin: Option, + /// Array of secrets. Each entry is a property string with a name and an optional value. + /// The value property contains the secret in base64 encoding. + /// For any API endpoints returning the endpoint config, + /// only the secret name but not the value will be returned. + /// When updating the config, also send all secrets that you want + /// to keep, setting only the name but not the value. Can be accessed from templates. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub secret: Vec>, +} + +#[api( + properties: { + name: { + schema: ENTITY_NAME_SCHEMA, + }, + secret: { + type: Array, + items: { + schema: KEY_AND_BASE64_VALUE_SCHEMA, + }, + optional: true, + }, + } +)] +#[derive(Serialize, Deserialize, Clone, Updater, Default)] +#[serde(rename_all = "kebab-case")] +/// Private configuration for Webhook notification endpoints. +/// This config will be saved to a separate configuration file with stricter +/// permissions (root:root 0600). +pub struct WebhookPrivateConfig { + /// Name of the endpoint + #[updater(skip)] + pub name: String, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + /// Array of secrets. Each entry is a property string with a name, + /// and a value property. The value property contains the secret + /// in base64 encoding. Can be accessed from templates. + pub secret: Vec>, +} + +/// A Webhook notification endpoint. +pub struct WebhookEndpoint { + pub config: WebhookConfig, + pub private_config: WebhookPrivateConfig, +} + +#[api] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Webhook configuration properties that can be deleted. +pub enum DeleteableWebhookProperty { + /// Delete `comment`. + Comment, + /// Delete `disable`. + Disable, + /// Delete `header`. + Header, + /// Delete `body`. + Body, + /// Delete `secret`. + Secret, +} + +#[api] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +/// Datatype used to represent key-value pairs, the value +/// being encoded in base64. +pub struct KeyAndBase64Val { + /// Name + pub name: String, + /// Base64 encoded value + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +impl KeyAndBase64Val { + #[cfg(test)] + pub fn new_with_plain_value(name: &str, value: &str) -> Self { + let value = base64::encode(value); + + Self { + name: name.into(), + value: Some(value), + } + } + + /// Decode the contained value, returning the plaintext value + /// + /// Returns an error if the contained value is not valid base64-encoded + /// text. + pub fn decode_value(&self) -> Result { + let value = self.value.as_deref().unwrap_or_default(); + let bytes = base64::decode(value).map_err(|_| { + Error::Generic(format!( + "could not decode base64 value with key '{}'", + self.name + )) + })?; + let value = String::from_utf8(bytes).map_err(|_| { + Error::Generic(format!( + "could not decode UTF8 string from base64, key '{}'", + self.name + )) + })?; + + Ok(value) + } +} + +pub const KEY_AND_BASE64_VALUE_SCHEMA: Schema = + StringSchema::new("String schema for pairs of keys and base64 encoded values") + .format(&ApiStringFormat::PropertyString( + &KeyAndBase64Val::API_SCHEMA, + )) + .schema(); + +impl Endpoint for WebhookEndpoint { + /// Send a notification to a webhook endpoint. + fn send(&self, notification: &Notification) -> Result<(), Error> { + let request = self.build_request(notification)?; + + self.create_client()? + .request(request) + .map_err(|err| self.mask_secret_in_error(err))?; + + Ok(()) + } + + /// Return the name of the endpoint. + fn name(&self) -> &str { + &self.config.name + } + + /// Check if the endpoint is disabled + fn disabled(&self) -> bool { + self.config.disable.unwrap_or_default() + } +} + +impl WebhookEndpoint { + fn create_client(&self) -> Result { + let proxy_config = context() + .http_proxy_config() + .map(|url| ProxyConfig::parse_proxy_url(&url)) + .transpose() + .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?; + + let options = HttpOptions { + proxy_config, + ..Default::default() + }; + + Ok(Client::new_with_timeout(options, HTTP_TIMEOUT)) + } + + fn build_request(&self, notification: &Notification) -> Result, Error> { + let (title, message) = match ¬ification.content { + Content::Template { + template_name, + data, + } => { + let rendered_title = + renderer::render_template(TemplateType::Subject, template_name, data)?; + let rendered_message = + renderer::render_template(TemplateType::PlaintextBody, template_name, data)?; + + (rendered_title, rendered_message) + } + #[cfg(feature = "mail-forwarder")] + Content::ForwardedMail { title, body, .. } => (title.clone(), body.clone()), + }; + + let mut fields = Map::new(); + + for (field_name, field_value) in ¬ification.metadata.additional_fields { + fields.insert(field_name.clone(), Value::String(field_value.to_string())); + } + + let mut secrets = Map::new(); + + for secret in &self.private_config.secret { + let value = secret.decode_value()?; + secrets.insert(secret.name.clone(), Value::String(value)); + } + + let data = json!({ + "title": &title, + "message": &message, + "severity": notification.metadata.severity, + "timestamp": notification.metadata.timestamp, + "fields": fields, + "secrets": secrets, + }); + + let handlebars = setup_handlebars(); + let body_template = self.base64_decode(self.config.body.as_deref().unwrap_or_default())?; + + let body = handlebars + .render_template(&body_template, &data) + .map_err(|err| self.mask_secret_in_error(err)) + .map_err(|err| Error::Generic(format!("failed to render webhook body: {err}")))?; + + let url = handlebars + .render_template(&self.config.url, &data) + .map_err(|err| self.mask_secret_in_error(err)) + .map_err(|err| Error::Generic(format!("failed to render webhook url: {err}")))?; + + let method: &str = self.config.method.into(); + let mut builder = http::Request::builder().uri(url).method(method); + + for header in &self.config.header { + let value = header.decode_value()?; + + let value = handlebars + .render_template(&value, &data) + .map_err(|err| self.mask_secret_in_error(err)) + .map_err(|err| { + Error::Generic(format!( + "failed to render header value template: {value}: {err}" + )) + })?; + + builder = builder.header(header.name.clone(), value); + } + + let request = builder + .body(body) + .map_err(|err| self.mask_secret_in_error(err)) + .map_err(|err| Error::Generic(format!("failed to build http request: {err}")))?; + + Ok(request) + } + + fn base64_decode(&self, s: &str) -> Result { + // Also here, TODO: revisit Error variants for the *whole* crate. + let s = base64::decode(s) + .map_err(|err| Error::Generic(format!("could not decode base64 value: {err}")))?; + + String::from_utf8(s).map_err(|err| { + Error::Generic(format!( + "base64 encoded value did not contain valid utf8: {err}" + )) + }) + } + + /// Mask secrets in errors to avoid them showing up in error messages and log files + /// + /// Use this for any error from third-party code where you are not 100% + /// sure whether it could leak the content of secrets in the error. + /// For instance, the http client will contain the URL, including + /// any URL parameters that could contain tokens. + /// + /// This function will only mask exact matches, but this should suffice + /// for the majority of cases. + fn mask_secret_in_error(&self, error: impl std::fmt::Display) -> Error { + let mut s = error.to_string(); + + for secret_value in &self.private_config.secret { + match secret_value.decode_value() { + Ok(value) => s = s.replace(&value, ""), + Err(e) => return e, + } + } + + Error::Generic(s) + } +} + +fn setup_handlebars() -> Handlebars<'static> { + let mut handlebars = Handlebars::new(); + + handlebars.register_helper("url-encode", Box::new(handlebars_percent_encode)); + handlebars.register_helper("json", Box::new(handlebars_json)); + handlebars.register_helper("escape", Box::new(handlebars_escape)); + + // There is no escape. + handlebars.register_escape_fn(handlebars::no_escape); + + handlebars +} + +fn handlebars_percent_encode( + h: &Helper, + _: &Handlebars, + _: &HandlebarsContext, + _rc: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + let param0 = h + .param(0) + .and_then(|v| v.value().as_str()) + .ok_or_else(|| HandlebarsRenderError::new("url-encode: missing parameter"))?; + + // See https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding + const FRAGMENT: &AsciiSet = &percent_encoding::CONTROLS + .add(b':') + .add(b'/') + .add(b'?') + .add(b'#') + .add(b'[') + .add(b']') + .add(b'@') + .add(b'!') + .add(b'$') + .add(b'&') + .add(b'\'') + .add(b'(') + .add(b')') + .add(b'*') + .add(b'+') + .add(b',') + .add(b';') + .add(b'=') + .add(b'%') + .add(b' '); + let a = percent_encoding::utf8_percent_encode(param0, FRAGMENT); + + out.write(&a.to_string())?; + + Ok(()) +} + +fn handlebars_json( + h: &Helper, + _: &Handlebars, + _: &HandlebarsContext, + _rc: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + let param0 = h + .param(0) + .map(|v| v.value()) + .ok_or_else(|| HandlebarsRenderError::new("json: missing parameter"))?; + + let json = serde_json::to_string(param0)?; + out.write(&json)?; + + Ok(()) +} + +fn handlebars_escape( + h: &Helper, + _: &Handlebars, + _: &HandlebarsContext, + _rc: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + let text = h + .param(0) + .and_then(|v| v.value().as_str()) + .ok_or_else(|| HandlebarsRenderError::new("escape: missing text parameter"))?; + + let val = Value::String(text.to_string()); + let json = serde_json::to_string(&val)?; + out.write(&json[1..json.len() - 1])?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::Severity; + + #[test] + fn test_build_request() -> Result<(), Error> { + let data = HashMap::from_iter([ + ("hello".into(), "hello world".into()), + ("test".into(), "escaped\nstring".into()), + ]); + + let body_template = r#" +{{ fields.test }} +{{ escape fields.test }} + +{{ json fields }} +{{ json fields.hello }} + +{{ url-encode fields.hello }} + +{{ json severity }} + +"#; + + let expected_body = r#" +escaped +string +escaped\nstring + +{"hello":"hello world","test":"escaped\nstring"} +"hello world" + +hello%20world + +"info" + +"#; + + let endpoint = WebhookEndpoint { + config: WebhookConfig { + name: "test".into(), + method: HttpMethod::Post, + url: "http://localhost/{{ url-encode fields.hello }}".into(), + header: vec![ + KeyAndBase64Val::new_with_plain_value("X-Severity", "{{ severity }}").into(), + ], + body: Some(base64::encode(body_template)), + ..Default::default() + }, + private_config: WebhookPrivateConfig { + name: "test".into(), + ..Default::default() + }, + }; + + let notification = Notification::from_template(Severity::Info, "foo", json!({}), data); + + let request = endpoint.build_request(¬ification)?; + + assert_eq!(request.uri(), "http://localhost/hello%20world"); + assert_eq!(request.body(), expected_body); + assert_eq!(request.method(), "POST"); + + assert_eq!(request.headers().get("X-Severity").unwrap(), "info"); + + Ok(()) + } +} diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs index 015d9b9c..12f3866b 100644 --- a/proxmox-notify/src/lib.rs +++ b/proxmox-notify/src/lib.rs @@ -500,6 +500,23 @@ impl Bus { ); } + #[cfg(feature = "webhook")] + { + use endpoints::webhook::WEBHOOK_TYPENAME; + use endpoints::webhook::{WebhookConfig, WebhookEndpoint, WebhookPrivateConfig}; + endpoints.extend( + parse_endpoints_with_private_config!( + config, + WebhookConfig, + WebhookPrivateConfig, + WebhookEndpoint, + WEBHOOK_TYPENAME + )? + .into_iter() + .map(|e| (e.name().into(), e)), + ); + } + let matchers = config .config .convert_to_typed_array(MATCHER_TYPENAME) diff --git a/proxmox-notify/src/renderer/mod.rs b/proxmox-notify/src/renderer/mod.rs index 8574a3fb..393cbbf2 100644 --- a/proxmox-notify/src/renderer/mod.rs +++ b/proxmox-notify/src/renderer/mod.rs @@ -290,7 +290,6 @@ pub fn render_template( (None, TemplateType::HtmlBody) => { ty = TemplateType::PlaintextBody; let plaintext_filename = format!("{template}-{suffix}", suffix = ty.file_suffix()); - log::info!("html template '{filename}' not found, falling back to plain text template '{plaintext_filename}'"); ( context::context().lookup_template(&plaintext_filename, None)?, true, @@ -329,8 +328,8 @@ mod tests { Some("1 KiB".to_string()) ); - assert_eq!(value_to_duration(&json!(60)), Some("1min ".to_string())); - assert_eq!(value_to_duration(&json!("60")), Some("1min ".to_string())); + assert_eq!(value_to_duration(&json!(60)), Some("1m".to_string())); + assert_eq!(value_to_duration(&json!("60")), Some("1m".to_string())); // The rendered value is in localtime, so we only check if the result is `Some`... // ... otherwise the test will break in another timezone :S diff --git a/proxmox-openid/debian/control b/proxmox-openid/debian/control index 447d4665..1b6af2b6 100644 --- a/proxmox-openid/debian/control +++ b/proxmox-openid/debian/control @@ -1,10 +1,10 @@ Source: rust-proxmox-openid Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-http-0.2+default-dev , @@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12), librust-ureq-2+gzip-dev (>= 2.4-~~) , librust-ureq-2+native-tls-dev (>= 2.4-~~) Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: Vcs-Browser: +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-openid Rules-Requires-Root: no @@ -54,5 +55,5 @@ Provides: librust-proxmox-openid-0.10+default-dev (= ${binary:Version}), librust-proxmox-openid-0.10.3-dev (= ${binary:Version}), librust-proxmox-openid-0.10.3+default-dev (= ${binary:Version}) -Description: Rust crate "proxmox-openid" - Rust source code +Description: Base for openid authentication in proxmox products - Rust source code Source code for Debianized Rust crate "proxmox-openid" diff --git a/proxmox-product-config/debian/control b/proxmox-product-config/debian/control index 67a03a98..847abd6f 100644 --- a/proxmox-product-config/debian/control +++ b/proxmox-product-config/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-product-config Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -11,9 +11,10 @@ Build-Depends: debhelper (>= 12), librust-proxmox-sys-0.6+default-dev , librust-proxmox-sys-0.6+timer-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-product-config Rules-Requires-Root: no diff --git a/proxmox-rest-server/Cargo.toml b/proxmox-rest-server/Cargo.toml index ef249abc..0c60bc54 100644 --- a/proxmox-rest-server/Cargo.toml +++ b/proxmox-rest-server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-rest-server" description = "REST server implementation" -version = "0.8.0" +version = "0.8.4" authors.workspace = true edition.workspace = true diff --git a/proxmox-rest-server/debian/changelog b/proxmox-rest-server/debian/changelog index 0a5b1632..f8d92f2e 100644 --- a/proxmox-rest-server/debian/changelog +++ b/proxmox-rest-server/debian/changelog @@ -1,3 +1,32 @@ +rust-proxmox-rest-server (0.8.4-1) bookworm; urgency=medium + + * add custom handlebars escape fn and skip escaping the '=' charater. This + is required to support base64 encoded values. + + -- Proxmox Support Team Mon, 25 Nov 2024 17:14:25 +0100 + +rust-proxmox-rest-server (0.8.3-1) bookworm; urgency=medium + + * connection: drop logging peek-length, which was only used for debugging + + -- Proxmox Support Team Fri, 15 Nov 2024 10:31:14 +0100 + +rust-proxmox-rest-server (0.8.2-1) bookworm; urgency=medium + + * fix #5868: rest-server: handshake detection: avoid infinite loop on + connections abort + + -- Proxmox Support Team Thu, 14 Nov 2024 14:34:09 +0100 + +rust-proxmox-rest-server (0.8.1-1) bookworm; urgency=medium + + * honor passed cipher suite & list in TlsAcceptorBuilder + + * provide better error messages in case key or certificate files are + inaccessible + + -- Proxmox Support Team Fri, 08 Nov 2024 12:02:10 +0100 + rust-proxmox-rest-server (0.8.0-1) bookworm; urgency=medium * rename old "streaming" api to "serializing" (as it was not truly diff --git a/proxmox-rest-server/debian/control b/proxmox-rest-server/debian/control index f091c19f..ab0a84a2 100644 --- a/proxmox-rest-server/debian/control +++ b/proxmox-rest-server/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-rest-server Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -20,7 +20,7 @@ Build-Depends: debhelper (>= 12), librust-proxmox-compression-0.2+default-dev (>= 0.2.4-~~) , librust-proxmox-daemon-0.1+default-dev , librust-proxmox-lang-1+default-dev (>= 1.3-~~) , - librust-proxmox-log-0.2+default-dev (>= 0.2.3-~~) , + librust-proxmox-log-0.2+default-dev (>= 0.2.5-~~) , librust-proxmox-router-3+default-dev , librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~) , librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) , @@ -43,9 +43,10 @@ Build-Depends: debhelper (>= 12), librust-tracing-0.1+default-dev , librust-url-2+default-dev (>= 2.2-~~) Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-rest-server Rules-Requires-Root: no @@ -68,7 +69,7 @@ Depends: librust-proxmox-compression-0.2+default-dev (>= 0.2.4-~~), librust-proxmox-daemon-0.1+default-dev, librust-proxmox-lang-1+default-dev (>= 1.3-~~), - librust-proxmox-log-0.2+default-dev (>= 0.2.3-~~), + librust-proxmox-log-0.2+default-dev (>= 0.2.5-~~), librust-proxmox-router-3+default-dev, librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~), librust-proxmox-schema-3+default-dev (>= 3.1.2-~~), @@ -99,8 +100,8 @@ Provides: librust-proxmox-rest-server-0+default-dev (= ${binary:Version}), librust-proxmox-rest-server-0.8-dev (= ${binary:Version}), librust-proxmox-rest-server-0.8+default-dev (= ${binary:Version}), - librust-proxmox-rest-server-0.8.0-dev (= ${binary:Version}), - librust-proxmox-rest-server-0.8.0+default-dev (= ${binary:Version}) + librust-proxmox-rest-server-0.8.4-dev (= ${binary:Version}), + librust-proxmox-rest-server-0.8.4+default-dev (= ${binary:Version}) Description: REST server implementation - Rust source code Source code for Debianized Rust crate "proxmox-rest-server" @@ -110,12 +111,12 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-proxmox-rest-server-dev (= ${binary:Version}), - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~), - librust-proxmox-http-0.9+rate-limited-stream-dev (>= 0.9.2-~~) + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+rate-limited-stream-dev (>= 0.9.4-~~) Provides: librust-proxmox-rest-server-0+rate-limited-stream-dev (= ${binary:Version}), librust-proxmox-rest-server-0.8+rate-limited-stream-dev (= ${binary:Version}), - librust-proxmox-rest-server-0.8.0+rate-limited-stream-dev (= ${binary:Version}) + librust-proxmox-rest-server-0.8.4+rate-limited-stream-dev (= ${binary:Version}) Description: REST server implementation - feature "rate-limited-stream" This metapackage enables feature "rate-limited-stream" for the Rust proxmox- rest-server crate, by pulling in any additional dependencies needed by that @@ -131,7 +132,7 @@ Depends: Provides: librust-proxmox-rest-server-0+templates-dev (= ${binary:Version}), librust-proxmox-rest-server-0.8+templates-dev (= ${binary:Version}), - librust-proxmox-rest-server-0.8.0+templates-dev (= ${binary:Version}) + librust-proxmox-rest-server-0.8.4+templates-dev (= ${binary:Version}) Description: REST server implementation - feature "templates" This metapackage enables feature "templates" for the Rust proxmox-rest-server crate, by pulling in any additional dependencies needed by that feature. diff --git a/proxmox-rest-server/src/api_config.rs b/proxmox-rest-server/src/api_config.rs index ddc37f22..7dbcad52 100644 --- a/proxmox-rest-server/src/api_config.rs +++ b/proxmox-rest-server/src/api_config.rs @@ -62,7 +62,7 @@ impl ApiConfig { privileged_addr: None, #[cfg(feature = "templates")] - templates: Default::default(), + templates: templates::Templates::with_escape_fn(), } } @@ -335,6 +335,31 @@ mod templates { } impl Templates { + pub fn with_escape_fn() -> Templates { + let mut registry = Handlebars::new(); + // This is the same as the default `html_escape` fn in handlebars, **but** it does not + // escape the '='. This is to preserve base64 values. + registry.register_escape_fn(|value| { + let mut output = String::new(); + for c in value.chars() { + match c { + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '&' => output.push_str("&"), + '\'' => output.push_str("'"), + '`' => output.push_str("`"), + _ => output.push(c), + } + } + output + }); + Self { + templates: RwLock::new(registry), + template_files: RwLock::new(HashMap::new()), + } + } + pub fn register

(&self, name: &str, path: P) -> Result<(), Error> where P: Into, diff --git a/proxmox-rest-server/src/connection.rs b/proxmox-rest-server/src/connection.rs index fbdfe96c..526555ae 100644 --- a/proxmox-rest-server/src/connection.rs +++ b/proxmox-rest-server/src/connection.rs @@ -12,13 +12,13 @@ use std::pin::{pin, Pin}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use anyhow::{format_err, Context as _, Error}; +use anyhow::{format_err, Context, Error}; use futures::FutureExt; use hyper::server::accept; use openssl::ec::{EcGroup, EcKey}; use openssl::nid::Nid; use openssl::pkey::{PKey, Private}; -use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +use openssl::ssl::{SslAcceptor, SslMethod}; use openssl::x509::X509; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc; @@ -78,6 +78,17 @@ impl TlsAcceptorBuilder { pub fn build(self) -> Result { let mut acceptor = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).unwrap(); + if let Some(cipher_suites) = self.cipher_suites.as_deref() { + acceptor + .set_ciphersuites(cipher_suites) + .context("failed to set tls acceptor cipher suites")?; + } + if let Some(cipher_list) = self.cipher_list.as_deref() { + acceptor + .set_cipher_list(cipher_list) + .context("failed to set tls acceptor cipher list")?; + } + match self.tls { Some(Tls::KeyCert(key, cert)) => { acceptor @@ -88,9 +99,17 @@ impl TlsAcceptorBuilder { .context("failed to set tls acceptor certificate")?; } Some(Tls::FilesPem(key, cert)) => { + let key_content = std::fs::read(&key) + .with_context(|| format!("Failed to read from private key file {key:?}"))?; acceptor - .set_private_key_file(key, SslFiletype::PEM) + .set_private_key(PKey::private_key_from_pem(&key_content)?.as_ref()) .context("failed to set tls acceptor private key file")?; + + { + // Check the permissions by opening the file + let _cert_fd = std::fs::File::open(&cert) + .with_context(|| format!("Failed to open certificate at {cert:?}"))?; + } acceptor .set_certificate_chain_file(cert) .context("failed to set tls acceptor certificate chain file")?; @@ -458,6 +477,7 @@ impl AcceptBuilder { const HANDSHAKE_BYTES_LEN: usize = 5; let future = async { + let mut previous_peek_len = 0; incoming_stream .async_io(tokio::io::Interest::READABLE, || { let mut buf = [0; HANDSHAKE_BYTES_LEN]; @@ -481,7 +501,14 @@ impl AcceptBuilder { // This means we will peek into the stream's queue until we got // HANDSHAKE_BYTE_LEN bytes or an error. Ok(peek_len) if peek_len < HANDSHAKE_BYTES_LEN => { - Err(io::ErrorKind::WouldBlock.into()) + // if we detect the same peek len again but still got a readable stream, + // the connection was probably closed, so abort here + if peek_len == previous_peek_len { + Err(io::ErrorKind::ConnectionAborted.into()) + } else { + previous_peek_len = peek_len; + Err(io::ErrorKind::WouldBlock.into()) + } } // Either we got Ok(HANDSHAKE_BYTES_LEN) or some error. res => res.map(|_| contains_tls_handshake_fragment(&buf)), diff --git a/proxmox-rest-server/src/worker_task.rs b/proxmox-rest-server/src/worker_task.rs index 6e76c2ca..beec691e 100644 --- a/proxmox-rest-server/src/worker_task.rs +++ b/proxmox-rest-server/src/worker_task.rs @@ -139,7 +139,7 @@ impl WorkerTaskSetup { .clone() .perm(nix::sys::stat::Mode::from_bits_truncate(0o660)); - let timeout = std::time::Duration::new(10, 0); + let timeout = std::time::Duration::new(15, 0); let file = proxmox_sys::fs::open_file_locked(&self.task_lock_fn, timeout, exclusive, options)?; @@ -923,7 +923,13 @@ impl WorkerTask { set_worker_count(hash.len()); } - setup.update_active_workers(Some(&upid))?; + // this wants to access WORKER_TASK_LIST, so we need to drop the lock above + let res = setup.update_active_workers(Some(&upid)); + if res.is_err() { + // needed to undo the insertion into WORKER_TASK_LIST above + worker.log_result(&res); + res? + } Ok((worker, logger)) } @@ -1017,8 +1023,11 @@ impl WorkerTask { self.log_message(state.result_text()); WORKER_TASK_LIST.lock().unwrap().remove(&self.upid.task_id); + // this wants to access WORKER_TASK_LIST, so we need to drop the lock above let _ = self.setup.update_active_workers(None); - set_worker_count(WORKER_TASK_LIST.lock().unwrap().len()); + // re-acquire the lock and hold it while updating the count + let lock = WORKER_TASK_LIST.lock().unwrap(); + set_worker_count(lock.len()); } /// Log a message. diff --git a/proxmox-router/debian/control b/proxmox-router/debian/control index 6e32cde5..896510d8 100644 --- a/proxmox-router/debian/control +++ b/proxmox-router/debian/control @@ -1,10 +1,10 @@ Source: rust-proxmox-router Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-env-logger-0.10+default-dev , @@ -25,9 +25,10 @@ Build-Depends: debhelper (>= 12), librust-serde-plain-1+default-dev , librust-unicode-width-0.1+default-dev (>= 0.1.8-~~) Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-router Rules-Requires-Root: no diff --git a/proxmox-rrd-api-types/debian/control b/proxmox-rrd-api-types/debian/control index e43033c9..4865f90d 100644 --- a/proxmox-rrd-api-types/debian/control +++ b/proxmox-rrd-api-types/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-rrd-api-types Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-plain-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-rrd-api-types Rules-Requires-Root: no diff --git a/proxmox-rrd/Cargo.toml b/proxmox-rrd/Cargo.toml index 9578ab3f..ce2139b5 100644 --- a/proxmox-rrd/Cargo.toml +++ b/proxmox-rrd/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-rrd" description = "Simple RRD database implementation." -version = "0.4.0" +version = "0.4.1" authors.workspace = true edition.workspace = true diff --git a/proxmox-rrd/debian/changelog b/proxmox-rrd/debian/changelog index 10e02eba..36762b1f 100644 --- a/proxmox-rrd/debian/changelog +++ b/proxmox-rrd/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-rrd (0.4.1-1) bookworm; urgency=medium + + * do not log three info-level messages on applying journal, one is enough + + -- Proxmox Support Team Mon, 25 Nov 2024 17:48:55 +0100 + rust-proxmox-rrd (0.4.0-1) bookworm; urgency=medium * drop api-types feature, the module was moved into its own crate diff --git a/proxmox-rrd/debian/control b/proxmox-rrd/debian/control index 48192ab4..cd87be52 100644 --- a/proxmox-rrd/debian/control +++ b/proxmox-rrd/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-rrd Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12), librust-serde-json-1+default-dev , librust-serde-plain-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-rrd Rules-Requires-Root: no @@ -55,8 +56,8 @@ Provides: librust-proxmox-rrd-0.4-dev (= ${binary:Version}), librust-proxmox-rrd-0.4+default-dev (= ${binary:Version}), librust-proxmox-rrd-0.4+rrd-v1-dev (= ${binary:Version}), - librust-proxmox-rrd-0.4.0-dev (= ${binary:Version}), - librust-proxmox-rrd-0.4.0+default-dev (= ${binary:Version}), - librust-proxmox-rrd-0.4.0+rrd-v1-dev (= ${binary:Version}) + librust-proxmox-rrd-0.4.1-dev (= ${binary:Version}), + librust-proxmox-rrd-0.4.1+default-dev (= ${binary:Version}), + librust-proxmox-rrd-0.4.1+rrd-v1-dev (= ${binary:Version}) Description: Simple RRD database implementation - Rust source code Source code for Debianized Rust crate "proxmox-rrd" diff --git a/proxmox-rrd/src/cache.rs b/proxmox-rrd/src/cache.rs index 40405037..9dd85a16 100644 --- a/proxmox-rrd/src/cache.rs +++ b/proxmox-rrd/src/cache.rs @@ -285,13 +285,9 @@ fn apply_and_commit_journal_thread( match apply_journal_impl(Arc::clone(&state), Arc::clone(&rrd_map)) { Ok(entries) => { let elapsed = start_time.elapsed().unwrap().as_secs_f64(); - log::info!( - "applied rrd journal ({} entries in {:.3} seconds)", - entries, - elapsed - ); + log::info!("applied rrd journal ({entries} entries in {elapsed:.3} seconds)"); } - Err(err) => bail!("apply rrd journal failed - {}", err), + Err(err) => bail!("apply rrd journal failed - {err}"), } } @@ -302,12 +298,10 @@ fn apply_and_commit_journal_thread( Ok(rrd_file_count) => { let elapsed = start_time.elapsed().unwrap().as_secs_f64(); log::info!( - "rrd journal successfully committed ({} files in {:.3} seconds)", - rrd_file_count, - elapsed + "rrd journal successfully committed ({rrd_file_count} files in {elapsed:.3} seconds)" ); } - Err(err) => bail!("rrd journal commit failed: {}", err), + Err(err) => bail!("rrd journal commit failed: {err}"), } Ok(()) } @@ -448,7 +442,7 @@ fn commit_journal_impl( let mut dir_set = BTreeSet::new(); - log::info!("write rrd data back to disk"); + log::debug!("write rrd data back to disk"); // save all RRDs - we only need a read lock here // Note: no fsync here (we do it afterwards) @@ -470,7 +464,7 @@ fn commit_journal_impl( // the likelihood that files are already synced, so this is // much faster (although we need to re-open the files). - log::info!("starting rrd data sync"); + log::debug!("starting rrd data sync"); for rel_path in files.iter() { let mut path = config.basedir.clone(); diff --git a/proxmox-schema/debian/control b/proxmox-schema/debian/control index b77d5752..4c4cb82f 100644 --- a/proxmox-schema/debian/control +++ b/proxmox-schema/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-schema Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12), librust-serde-json-1+default-dev , librust-textwrap-0.16+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-schema Rules-Requires-Root: no diff --git a/proxmox-section-config/debian/control b/proxmox-section-config/debian/control index ad14c647..d6765156 100644 --- a/proxmox-section-config/debian/control +++ b/proxmox-section-config/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-section-config Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-section-config Rules-Requires-Root: no diff --git a/proxmox-sendmail/Cargo.toml b/proxmox-sendmail/Cargo.toml new file mode 100644 index 00000000..e04e2595 --- /dev/null +++ b/proxmox-sendmail/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "proxmox-sendmail" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +exclude.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +base64 = { workspace = true } +percent-encoding = { workspace = true } +proxmox-time = { workspace = true } + +[features] +default = [] +mail-forwarder = [] diff --git a/proxmox-sendmail/debian/changelog b/proxmox-sendmail/debian/changelog new file mode 100644 index 00000000..71d7c9f8 --- /dev/null +++ b/proxmox-sendmail/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-sendmail (0.1.0-1) bookworm; urgency=medium + + * Initial release. + + -- Proxmox Support Team Mon, 02 Dec 2024 14:47:42 +0100 diff --git a/proxmox-sendmail/debian/control b/proxmox-sendmail/debian/control new file mode 100644 index 00000000..dfc8b9bf --- /dev/null +++ b/proxmox-sendmail/debian/control @@ -0,0 +1,43 @@ +Source: rust-proxmox-sendmail +Section: rust +Priority: optional +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, + cargo:native , + rustc:native (>= 1.80) , + libstd-rust-dev , + librust-anyhow-1+default-dev , + librust-base64-0.13+default-dev , + librust-percent-encoding-2+default-dev (>= 2.1-~~) , + librust-proxmox-time-2+default-dev +Maintainer: Proxmox Support Team +Standards-Version: 4.7.0 +Vcs-Git: git://git.proxmox.com/git/proxmox.git +Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com +X-Cargo-Crate: proxmox-sendmail +Rules-Requires-Root: no + +Package: librust-proxmox-sendmail-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-anyhow-1+default-dev, + librust-base64-0.13+default-dev, + librust-percent-encoding-2+default-dev (>= 2.1-~~), + librust-proxmox-time-2+default-dev +Provides: + librust-proxmox-sendmail+default-dev (= ${binary:Version}), + librust-proxmox-sendmail+mail-forwarder-dev (= ${binary:Version}), + librust-proxmox-sendmail-0-dev (= ${binary:Version}), + librust-proxmox-sendmail-0+default-dev (= ${binary:Version}), + librust-proxmox-sendmail-0+mail-forwarder-dev (= ${binary:Version}), + librust-proxmox-sendmail-0.1-dev (= ${binary:Version}), + librust-proxmox-sendmail-0.1+default-dev (= ${binary:Version}), + librust-proxmox-sendmail-0.1+mail-forwarder-dev (= ${binary:Version}), + librust-proxmox-sendmail-0.1.0-dev (= ${binary:Version}), + librust-proxmox-sendmail-0.1.0+default-dev (= ${binary:Version}), + librust-proxmox-sendmail-0.1.0+mail-forwarder-dev (= ${binary:Version}) +Description: Rust crate "proxmox-sendmail" - Rust source code + Source code for Debianized Rust crate "proxmox-sendmail" diff --git a/proxmox-sendmail/debian/copyright b/proxmox-sendmail/debian/copyright new file mode 100644 index 00000000..b227c290 --- /dev/null +++ b/proxmox-sendmail/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2019 - 2024 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see . diff --git a/proxmox-sendmail/debian/debcargo.toml b/proxmox-sendmail/debian/debcargo.toml new file mode 100644 index 00000000..b7864cdb --- /dev/null +++ b/proxmox-sendmail/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox-sendmail/src/lib.rs b/proxmox-sendmail/src/lib.rs new file mode 100644 index 00000000..e7e2982f --- /dev/null +++ b/proxmox-sendmail/src/lib.rs @@ -0,0 +1,836 @@ +//! +//! This library implements the [`Mail`] trait which makes it easy to send emails with attachments +//! and alternative html parts to one or multiple receivers via ``sendmail``. +//! + +use std::io::Write; +use std::process::{Command, Stdio}; + +use anyhow::{bail, Context, Error}; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; + +// Characters in this set will be encoded, so reproduce the inverse of the set described by RFC5987 +// Section 3.2.1 `attr-char`, as that describes all characters that **don't** need encoding: +// +// https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1 +// +// `CONTROLS` contains all control characters 0x00 - 0x1f and 0x7f as well as all non-ascii +// characters, so we need to add all characters here that aren't described in `attr-char` that are +// in the range 0x20-0x7e +const RFC5987SET: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'%') + .add(b'&') + .add(b'\'') + .add(b'(') + .add(b')') + .add(b'*') + .add(b',') + .add(b'/') + .add(b':') + .add(b';') + .add(b'<') + .add(b'=') + .add(b'>') + .add(b'?') + .add(b'@') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'{') + .add(b'}'); + +struct Recipient { + name: Option, + email: String, +} + +impl Recipient { + // Returns true if the name of the recipient is undefined or contains only ascii characters + fn is_ascii(&self) -> bool { + self.name.as_ref().map(|n| n.is_ascii()).unwrap_or(true) + } + + fn format_recipient(&self) -> String { + if let Some(name) = &self.name { + if !name.is_ascii() { + format!("=?utf-8?B?{}?= <{}>", base64::encode(name), self.email) + } else { + format!("{name} <{}>", self.email) + } + } else { + self.email.to_string() + } + } +} + +struct Attachment<'a> { + filename: String, + mime: String, + content: &'a [u8], +} + +impl<'a> Attachment<'a> { + fn format_attachment(&self, file_boundary: &str) -> String { + use std::fmt::Write; + + let mut attachment = String::new(); + + let _ = writeln!(attachment, "\n--{file_boundary}"); + let _ = writeln!( + attachment, + "Content-Type: {}; name=\"{}\"", + self.mime, self.filename + ); + + // both `filename` and `filename*` are included for additional compatability + let _ = writeln!( + attachment, + "Content-Disposition: attachment; filename=\"{}\"; filename*=UTF-8''{}", + self.filename, + utf8_percent_encode(&self.filename, RFC5987SET) + ); + attachment.push_str("Content-Transfer-Encoding: base64\n\n"); + + // base64 encode the attachment and hard-wrap the base64 encoded string every 72 + // characters. this improves compatability. + attachment.push_str( + &base64::encode(self.content) + .chars() + .enumerate() + .flat_map(|(i, c)| { + if i != 0 && i % 72 == 0 { + Some('\n') + } else { + None + } + .into_iter() + .chain(std::iter::once(c)) + }) + .collect::(), + ); + + attachment + } +} + +/// This struct is used to define mails that are to be sent via the `sendmail` command. +pub struct Mail<'a> { + mail_author: String, + mail_from: String, + subject: String, + to: Vec, + body_txt: String, + body_html: Option, + attachments: Vec>, + mask_participants: bool, +} + +impl<'a> Mail<'a> { + /// Creates a new mail with a mail author, from address, subject line and a plain text body. + /// + /// Note: If the author's name or the subject line contains UTF-8 characters they will be + /// appropriately encoded. + pub fn new(mail_author: &str, mail_from: &str, subject: &str, body_txt: &str) -> Self { + Self { + mail_author: mail_author.to_string(), + mail_from: mail_from.to_string(), + subject: subject.to_string(), + to: Vec::new(), + body_txt: body_txt.to_string(), + body_html: None, + attachments: Vec::new(), + mask_participants: true, + } + } + + /// Adds a recipient to the mail without specifying a name separately. + /// + /// Note: No formatting or encoding will be done here, the value will be passed to the `To:` + /// header directly. + pub fn add_recipient(&mut self, email: &str) { + self.to.push(Recipient { + name: None, + email: email.to_string(), + }); + } + + /// Builder-pattern method to conveniently add a recipient to an email without specifying a + /// name separately. + /// + /// Note: No formatting or encoding will be done here, the value will be passed to the `To:` + /// header directly. + pub fn with_recipient(mut self, email: &str) -> Self { + self.add_recipient(email); + self + } + + /// Adds a recipient to the mail with a name. + /// + /// Notes: + /// + /// - If the name contains UTF-8 characters it will be encoded. Then the possibly encoded name + /// and non-encoded email address will be passed to the `To:` header in this format: + /// `{encoded_name} <{email}>` + /// - If multiple receivers are specified, they will be masked so as not to disclose them to + /// other receivers. This can be disabled via [`Mail::unmask_recipients`] or + /// [`Mail::with_unmasked_recipients`]. + pub fn add_recipient_and_name(&mut self, name: &str, email: &str) { + self.to.push(Recipient { + name: Some(name.to_string()), + email: email.to_string(), + }); + } + + /// Builder-style method to conveniently add a recipient with a name to an email. + /// + /// Notes: + /// + /// - If the name contains UTF-8 characters it will be encoded. Then the possibly encoded name + /// and non-encoded email address will be passed to the `To:` header in this format: + /// `{encoded_name} <{email}>` + /// - If multiple receivers are specified, they will be masked so as not to disclose them to + /// other receivers. This can be disabled via [`Mail::unmask_recipients`] or + /// [`Mail::with_unmasked_recipients`]. + pub fn with_recipient_and_name(mut self, name: &str, email: &str) -> Self { + self.add_recipient_and_name(name, email); + self + } + + /// Adds an attachment with a specified file name and mime-type to an email. + /// + /// Note: Adding attachments triggers `multipart/mixed` mode. + pub fn add_attachment(&mut self, filename: &str, mime_type: &str, content: &'a [u8]) { + self.attachments.push(Attachment { + filename: filename.to_string(), + mime: mime_type.to_string(), + content, + }); + } + + /// Builder-style method to conveniently add an attachment with a specific filename and + /// mime-type to an email. + /// + /// Note: Adding attachements triggers `multipart/mixed` mode. + pub fn with_attachment(mut self, filename: &str, mime_type: &str, content: &'a [u8]) -> Self { + self.add_attachment(filename, mime_type, content); + self + } + + /// Set an alternative HTML part. + /// + /// Note: This triggers `multipart/alternative` mode. If both an HTML part and at least one + /// attachment are specified, the `multipart/alternative` part will be nested within the first + /// `multipart/mixed` part. This should ensure that the HTML is displayed properly by client's + /// that prioritize it over the plain text part (should be the default for most clients) while + /// also properly displaying the attachments. + pub fn set_html_alt(&mut self, body_html: &str) { + self.body_html.replace(body_html.to_string()); + } + + /// Builder-style method to add an alternative HTML part. + /// + /// Note: This triggers `multipart/alternative` mode. If both an HTML part and at least one + /// attachment are specified, the `multipart/alternative` part will be nested within the first + /// `multipart/mixed` part. This should ensure that the HTML is displayed properly by client's + /// that prioritize it over the plain text part (should be the default for most clients) while + /// also properly displaying the attachments. + pub fn with_html_alt(mut self, body_html: &str) -> Self { + self.set_html_alt(body_html); + self + } + + /// This function ensures that recipients of the mail are not masked. Being able to see all + /// recipients of a mail can be helpful in, for example, notification scenarios. + pub fn unmask_recipients(&mut self) { + self.mask_participants = false; + } + + /// Builder-style function that ensures that recipients of the mail are not masked. Being able + /// to see all recipients of a mail can be helpful in, for example, notification scenarios. + pub fn with_unmasked_recipients(mut self) -> Self { + self.unmask_recipients(); + self + } + + /// Sends the email. This will fail if no recipients have been added. + /// + /// Note: An `Auto-Submitted: auto-generated` header is added to avoid triggering OOO and + /// similar mails. + pub fn send(&self) -> Result<(), Error> { + if self.to.is_empty() { + bail!("no recipients provided for the mail, cannot send it."); + } + + let now = proxmox_time::epoch_i64(); + let body = self.format_mail(now)?; + + let mut sendmail_process = Command::new("/usr/sbin/sendmail") + .arg("-B") + .arg("8BITMIME") + .arg("-f") + .arg(&self.mail_from) + .arg("--") + .args(self.to.iter().map(|p| &p.email).collect::>()) + .stdin(Stdio::piped()) + .spawn() + .with_context(|| "could not spawn sendmail process")?; + + sendmail_process + .stdin + .as_ref() + .unwrap() + .write_all(body.as_bytes()) + .with_context(|| "couldn't write to sendmail stdin")?; + + sendmail_process + .wait() + .with_context(|| "sendmail did not exit successfully")?; + + Ok(()) + } + + /// Forwards an email message to a given list of recipients. + /// + /// `message` must be compatible with ``sendmail`` (the message is piped into stdin unmodified). + #[cfg(feature = "mail-forwarder")] + pub fn forward( + mailto: &[&str], + mailfrom: &str, + message: &[u8], + uid: Option, + ) -> Result<(), Error> { + use std::os::unix::process::CommandExt; + + if mailto.is_empty() { + bail!("At least one recipient has to be specified!"); + } + + let mut builder = Command::new("/usr/sbin/sendmail"); + + builder + .args([ + "-N", "never", // never send DSN (avoid mail loops) + "-f", mailfrom, "--", + ]) + .args(mailto) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + if let Some(uid) = uid { + builder.uid(uid); + } + + let mut sendmail_process = builder + .spawn() + .with_context(|| "could not spawn sendmail process")?; + + sendmail_process + .stdin + .take() + .unwrap() + .write_all(message) + .with_context(|| "couldn't write to sendmail stdin")?; + + sendmail_process + .wait() + .with_context(|| "sendmail did not exit successfully")?; + + Ok(()) + } + + fn format_mail(&self, now: i64) -> Result { + use std::fmt::Write; + + let file_boundary = format!("----_=_NextPart_001_{now}"); + let html_boundary = format!("----_=_NextPart_002_{now}"); + + let mut mail = self.format_header(now, &file_boundary, &html_boundary)?; + mail.push_str(&self.format_body(&file_boundary, &html_boundary)?); + + if !self.attachments.is_empty() { + mail.push_str( + &self + .attachments + .iter() + .map(|a| a.format_attachment(&file_boundary)) + .collect::(), + ); + + write!(mail, "\n--{file_boundary}--")?; + } + + Ok(mail) + } + + fn format_header( + &self, + now: i64, + file_boundary: &str, + html_boundary: &str, + ) -> Result { + use std::fmt::Write; + + let mut header = String::new(); + + let encoded_to = if self.to.len() > 1 && self.mask_participants { + // if the receivers are masked, we know that they don't need to be encoded + false + } else { + // check if there is a recipient that needs encoding + self.to.iter().any(|r| !r.is_ascii()) + }; + + if !self.attachments.is_empty() { + header.push_str("Content-Type: multipart/mixed;\n"); + writeln!(header, "\tboundary=\"{file_boundary}\"")?; + header.push_str("MIME-Version: 1.0\n"); + } else if self.body_html.is_some() { + header.push_str("Content-Type: multipart/alternative;\n"); + writeln!(header, "\tboundary=\"{html_boundary}\"")?; + header.push_str("MIME-Version: 1.0\n"); + } else if !self.subject.is_ascii() || !self.mail_author.is_ascii() || encoded_to { + header.push_str("MIME-Version: 1.0\n"); + } + + if !self.subject.is_ascii() { + writeln!( + header, + "Subject: =?utf-8?B?{}?=", + base64::encode(&self.subject) + )?; + } else { + writeln!(header, "Subject: {}", self.subject)?; + }; + + if !self.mail_author.is_ascii() { + writeln!( + header, + "From: =?utf-8?B?{}?= <{}>", + base64::encode(&self.mail_author), + self.mail_from + )?; + } else { + writeln!(header, "From: {} <{}>", self.mail_author, self.mail_from)?; + } + + let to = if self.to.len() > 1 && self.mask_participants { + // don't disclose all recipients if the mail goes out to multiple + let recipient = Recipient { + name: Some("Undisclosed".to_string()), + email: "noreply".to_string(), + }; + + recipient.format_recipient() + } else { + self.to + .iter() + .map(Recipient::format_recipient) + .collect::>() + .join(", ") + }; + + writeln!(header, "To: {to}")?; + + let rfc2822_date = proxmox_time::epoch_to_rfc2822(now) + .with_context(|| "could not convert epoch to rfc2822 date")?; + writeln!(header, "Date: {rfc2822_date}")?; + header.push_str("Auto-Submitted: auto-generated;\n"); + + Ok(header) + } + + fn format_body(&self, file_boundary: &str, html_boundary: &str) -> Result { + use std::fmt::Write; + + let mut body = String::new(); + + if self.body_html.is_some() && !self.attachments.is_empty() { + body.push_str("\nThis is a multi-part message in MIME format.\n"); + writeln!(body, "\n--{file_boundary}")?; + writeln!( + body, + "Content-Type: multipart/alternative; boundary=\"{html_boundary}\"" + )?; + body.push_str("MIME-Version: 1.0\n"); + writeln!(body, "\n--{html_boundary}")?; + } else if self.body_html.is_some() { + body.push_str("\nThis is a multi-part message in MIME format.\n"); + writeln!(body, "\n--{html_boundary}")?; + } else if self.body_html.is_none() && !self.attachments.is_empty() { + body.push_str("\nThis is a multi-part message in MIME format.\n"); + writeln!(body, "\n--{file_boundary}")?; + } + + body.push_str("Content-Type: text/plain;\n"); + body.push_str("\tcharset=\"UTF-8\"\n"); + body.push_str("Content-Transfer-Encoding: 8bit\n\n"); + body.push_str(&self.body_txt); + + if let Some(html) = &self.body_html { + writeln!(body, "\n--{html_boundary}")?; + body.push_str("Content-Type: text/html;\n"); + body.push_str("\tcharset=\"UTF-8\"\n"); + body.push_str("Content-Transfer-Encoding: 8bit\n\n"); + body.push_str(html); + write!(body, "\n--{html_boundary}--")?; + } + + Ok(body) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn email_without_recipients_fails() { + let result = Mail::new("Sender", "mail@example.com", "hi", "body").send(); + assert!(result.is_err()); + } + + #[test] + #[cfg(feature = "mail-forwarder")] + fn forwarding_without_recipients_fails() { + let result = Mail::forward(&[], "me@example.com", String::from("text").as_bytes(), None); + assert!(result.is_err()); + } + + #[test] + fn simple_ascii_text_mail() { + let mail = Mail::new( + "Sender Name", + "mailfrom@example.com", + "Subject Line", + "This is just ascii text.\nNothing too special.", + ) + .with_recipient_and_name("Receiver Name", "receiver@example.com"); + + let body = mail.format_mail(0).expect("could not format mail"); + + assert_eq!( + body, + r#"Subject: Subject Line +From: Sender Name +To: Receiver Name +Date: Thu, 01 Jan 1970 01:00:00 +0100 +Auto-Submitted: auto-generated; +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +This is just ascii text. +Nothing too special."# + ) + } + + #[test] + fn multiple_receiver_masked() { + let mail = Mail::new( + "Sender Name", + "mailfrom@example.com", + "Subject Line", + "This is just ascii text.\nNothing too special.", + ) + .with_recipient_and_name("Receiver Name", "receiver@example.com") + .with_recipient("two@example.com") + .with_recipient_and_name("mäx müstermänn", "mm@example.com"); + + let body = mail.format_mail(0).expect("could not format mail"); + + assert_eq!( + body, + r#"Subject: Subject Line +From: Sender Name +To: Undisclosed +Date: Thu, 01 Jan 1970 01:00:00 +0100 +Auto-Submitted: auto-generated; +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +This is just ascii text. +Nothing too special."# + ) + } + + #[test] + fn multiple_receiver_unmasked() { + let mail = Mail::new( + "Sender Name", + "mailfrom@example.com", + "Subject Line", + "This is just ascii text.\nNothing too special.", + ) + .with_recipient_and_name("Receiver Name", "receiver@example.com") + .with_recipient("two@example.com") + .with_recipient_and_name("mäx müstermänn", "mm@example.com") + .with_unmasked_recipients(); + + let body = mail.format_mail(0).expect("could not format mail"); + + assert_eq!( + body, + r#"MIME-Version: 1.0 +Subject: Subject Line +From: Sender Name +To: Receiver Name , two@example.com, =?utf-8?B?bcOkeCBtw7xzdGVybcOkbm4=?= +Date: Thu, 01 Jan 1970 01:00:00 +0100 +Auto-Submitted: auto-generated; +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +This is just ascii text. +Nothing too special."# + ) + } + + #[test] + fn simple_utf8_text_mail() { + let mail = Mail::new( + "UTF-8 Sender Name 📧", + "differentfrom@example.com", + "Subject Line 🧑", + "This utf-8 email should handle emojis\n🧑📧\nand weird german characters: öäüß\nand more.", + ) + .with_recipient_and_name("Receiver Name📩", "receiver@example.com"); + + let body = mail.format_mail(1732806251).expect("could not format mail"); + + assert_eq!( + body, + r#"MIME-Version: 1.0 +Subject: =?utf-8?B?U3ViamVjdCBMaW5lIPCfp5E=?= +From: =?utf-8?B?VVRGLTggU2VuZGVyIE5hbWUg8J+Tpw==?= +To: =?utf-8?B?UmVjZWl2ZXIgTmFtZfCfk6k=?= +Date: Thu, 28 Nov 2024 16:04:11 +0100 +Auto-Submitted: auto-generated; +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +This utf-8 email should handle emojis +🧑📧 +and weird german characters: öäüß +and more."# + ) + } + + #[test] + fn multipart_html_alternative() { + let mail = Mail::new( + "Sender Name", + "from@example.com", + "Subject Line", + "Lorem Ipsum Dolor Sit\nAmet", + ) + .with_recipient("receiver@example.com") + .with_html_alt("\n\t

\n\t\tLorem Ipsum Dolor Sit Amet\n\t
\n"); + let body = mail.format_mail(1732806251).expect("could not format mail"); + assert_eq!( + body, + r#"Content-Type: multipart/alternative; + boundary="----_=_NextPart_002_1732806251" +MIME-Version: 1.0 +Subject: Subject Line +From: Sender Name +To: receiver@example.com +Date: Thu, 28 Nov 2024 16:04:11 +0100 +Auto-Submitted: auto-generated; + +This is a multi-part message in MIME format. + +------_=_NextPart_002_1732806251 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +Lorem Ipsum Dolor Sit +Amet +------_=_NextPart_002_1732806251 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + + +
+		Lorem Ipsum Dolor Sit Amet
+	
+ +------_=_NextPart_002_1732806251--"# + ) + } + + #[test] + fn multipart_plain_text_attachments_mixed() { + let bin: [u8; 62] = [ + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + ]; + + let mail = Mail::new( + "Sender Name", + "from@example.com", + "Subject Line", + "Lorem Ipsum Dolor Sit\nAmet", + ) + .with_recipient_and_name("Receiver Name", "receiver@example.com") + .with_attachment("deadbeef.bin", "application/octet-stream", &bin); + + let body = mail.format_mail(1732806251).expect("could not format mail"); + assert_eq!( + body, + r#"Content-Type: multipart/mixed; + boundary="----_=_NextPart_001_1732806251" +MIME-Version: 1.0 +Subject: Subject Line +From: Sender Name +To: Receiver Name +Date: Thu, 28 Nov 2024 16:04:11 +0100 +Auto-Submitted: auto-generated; + +This is a multi-part message in MIME format. + +------_=_NextPart_001_1732806251 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +Lorem Ipsum Dolor Sit +Amet +------_=_NextPart_001_1732806251 +Content-Type: application/octet-stream; name="deadbeef.bin" +Content-Disposition: attachment; filename="deadbeef.bin"; filename*=UTF-8''deadbeef.bin +Content-Transfer-Encoding: base64 + +3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v +3q2+796tvu8= +------_=_NextPart_001_1732806251--"# + ) + } + + #[test] + fn multipart_plain_text_html_alternative_attachments() { + let bin: [u8; 62] = [ + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, + ]; + + let mail = Mail::new( + "Sender Name", + "from@example.com", + "Subject Line", + "Lorem Ipsum Dolor Sit\nAmet", + ) + .with_recipient_and_name("Receiver Name", "receiver@example.com") + .with_attachment("deadbeef.bin", "application/octet-stream", &bin) + .with_attachment("🐄💀.bin", "image/bmp", &bin) + .with_html_alt("\n\t
\n\t\tLorem Ipsum Dolor Sit Amet\n\t
\n"); + + let body = mail.format_mail(1732806251).expect("could not format mail"); + + assert_eq!( + body, + r#"Content-Type: multipart/mixed; + boundary="----_=_NextPart_001_1732806251" +MIME-Version: 1.0 +Subject: Subject Line +From: Sender Name +To: Receiver Name +Date: Thu, 28 Nov 2024 16:04:11 +0100 +Auto-Submitted: auto-generated; + +This is a multi-part message in MIME format. + +------_=_NextPart_001_1732806251 +Content-Type: multipart/alternative; boundary="----_=_NextPart_002_1732806251" +MIME-Version: 1.0 + +------_=_NextPart_002_1732806251 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +Lorem Ipsum Dolor Sit +Amet +------_=_NextPart_002_1732806251 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + + +
+		Lorem Ipsum Dolor Sit Amet
+	
+ +------_=_NextPart_002_1732806251-- +------_=_NextPart_001_1732806251 +Content-Type: application/octet-stream; name="deadbeef.bin" +Content-Disposition: attachment; filename="deadbeef.bin"; filename*=UTF-8''deadbeef.bin +Content-Transfer-Encoding: base64 + +3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v +3q2+796tvu8= +------_=_NextPart_001_1732806251 +Content-Type: image/bmp; name="🐄💀.bin" +Content-Disposition: attachment; filename="🐄💀.bin"; filename*=UTF-8''%F0%9F%90%84%F0%9F%92%80.bin +Content-Transfer-Encoding: base64 + +3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v +3q2+796tvu8= +------_=_NextPart_001_1732806251--"# + ) + } + + #[test] + fn test_format_mail_multipart() { + let mail = Mail::new( + "Fred Oobar", + "foobar@example.com", + "This is the subject", + "This is the plain body", + ) + .with_recipient_and_name("Tony Est", "test@example.com") + .with_html_alt("This is the HTML body"); + + let body = mail.format_mail(1718977850).expect("could not format mail"); + + assert_eq!( + body, + r#"Content-Type: multipart/alternative; + boundary="----_=_NextPart_002_1718977850" +MIME-Version: 1.0 +Subject: This is the subject +From: Fred Oobar +To: Tony Est +Date: Fri, 21 Jun 2024 15:50:50 +0200 +Auto-Submitted: auto-generated; + +This is a multi-part message in MIME format. + +------_=_NextPart_002_1718977850 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +This is the plain body +------_=_NextPart_002_1718977850 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: 8bit + +This is the HTML body +------_=_NextPart_002_1718977850--"# + ); + } +} diff --git a/proxmox-serde/debian/control b/proxmox-serde/debian/control index 041d2c84..1ea984fe 100644 --- a/proxmox-serde/debian/control +++ b/proxmox-serde/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-serde Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , uuid-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-serde Rules-Requires-Root: no diff --git a/proxmox-shared-cache/debian/control b/proxmox-shared-cache/debian/control index 90f6ebfe..34840625 100644 --- a/proxmox-shared-cache/debian/control +++ b/proxmox-shared-cache/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-shared-cache Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -15,9 +15,10 @@ Build-Depends: debhelper (>= 12), librust-serde-json-1+default-dev , librust-serde-json-1+raw-value-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/proxmox-shared-cache] Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/proxmox-shared-cache +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-shared-cache Rules-Requires-Root: no diff --git a/proxmox-shared-memory/debian/control b/proxmox-shared-memory/debian/control index 9926fc7c..3be5372f 100644 --- a/proxmox-shared-memory/debian/control +++ b/proxmox-shared-memory/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-shared-memory Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -11,9 +11,10 @@ Build-Depends: debhelper (>= 12), librust-nix-0.26+default-dev (>= 0.26.1-~~) , librust-proxmox-sys-0.6+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-shared-memory Rules-Requires-Root: no diff --git a/proxmox-simple-config/debian/control b/proxmox-simple-config/debian/control index d04bdc43..966ba8dd 100644 --- a/proxmox-simple-config/debian/control +++ b/proxmox-simple-config/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-simple-config Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -14,9 +14,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-simple-config Rules-Requires-Root: no diff --git a/proxmox-sortable-macro/debian/control b/proxmox-sortable-macro/debian/control index 26684d5e..4ddbd01c 100644 --- a/proxmox-sortable-macro/debian/control +++ b/proxmox-sortable-macro/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-sortable-macro Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12), librust-syn-2+full-dev , librust-syn-2+visit-mut-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.1 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-sortable-macro Rules-Requires-Root: no @@ -37,5 +38,4 @@ Provides: librust-proxmox-sortable-macro-0.1.3-dev (= ${binary:Version}), librust-proxmox-sortable-macro-0.1.3+default-dev (= ${binary:Version}) Description: Proxmox sortable macro - Rust source code - This package contains the source for the Rust proxmox-sortable-macro crate, - packaged by debcargo for use with cargo and dh-cargo. + Source code for Debianized Rust crate "proxmox-sortable-macro" diff --git a/proxmox-subscription/Cargo.toml b/proxmox-subscription/Cargo.toml index cb5fd512..cabc9a0d 100644 --- a/proxmox-subscription/Cargo.toml +++ b/proxmox-subscription/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-subscription" description = "Proxmox subscription utilitites" -version = "0.4.6" +version = "0.5.0" authors.workspace = true edition.workspace = true @@ -13,20 +13,21 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true -base64.workspace = true -hex.workspace = true -openssl.workspace = true +base64 = { workspace = true, optional = true } +hex = { workspace = true, optional = true } +openssl = { workspace = true, optional = true } regex.workspace = true serde.workspace = true serde_json.workspace = true -proxmox-http = { workspace = true, features = ["client-trait", "http-helpers"] } +proxmox-http = { workspace = true, optional = true, features = ["client-trait", "http-helpers"] } proxmox-serde.workspace = true -proxmox-sys.workspace = true -proxmox-time.workspace = true +proxmox-sys = { workspace = true, optional = true } +proxmox-time = { workspace = true, optional = true } proxmox-schema = { workspace = true, features = ["api-macro"], optional = true } [features] -default = [] +default = ["impl"] +impl = [ "dep:base64", "dep:hex", "dep:openssl", "dep:proxmox-http", "dep:proxmox-sys", "dep:proxmox-time"] api-types = ["dep:proxmox-schema"] diff --git a/proxmox-subscription/debian/changelog b/proxmox-subscription/debian/changelog index 84eac05b..a88cd820 100644 --- a/proxmox-subscription/debian/changelog +++ b/proxmox-subscription/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-subscription (0.5.0-1) bookworm; urgency=medium + + * move most of the implmentation into `impl` feature + + -- Proxmox Support Team Thu, 07 Nov 2024 12:31:46 +0100 + rust-proxmox-subscription (0.4.6-1) bookworm; urgency=medium * replace lazy_static with std's LazyLock and drop the dependency diff --git a/proxmox-subscription/debian/control b/proxmox-subscription/debian/control index faa97d1d..300ff90d 100644 --- a/proxmox-subscription/debian/control +++ b/proxmox-subscription/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-subscription Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -10,9 +10,9 @@ Build-Depends: debhelper (>= 12), librust-base64-0.13+default-dev , librust-hex-0.4+default-dev , librust-openssl-0.10+default-dev , - librust-proxmox-http-0.9+client-trait-dev (>= 0.9.2-~~) , - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) , - librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.2-~~) , + librust-proxmox-http-0.9+client-trait-dev (>= 0.9.4-~~) , + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) , + librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.4-~~) , librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~) , librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~) , librust-proxmox-sys-0.6+default-dev , @@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-subscription Rules-Requires-Root: no @@ -33,29 +34,19 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-anyhow-1+default-dev, - librust-base64-0.13+default-dev, - librust-hex-0.4+default-dev, - librust-openssl-0.10+default-dev, - librust-proxmox-http-0.9+client-trait-dev (>= 0.9.2-~~), - librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~), - librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.2-~~), librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~), librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~), - librust-proxmox-sys-0.6+default-dev, - librust-proxmox-time-2+default-dev, librust-regex-1+default-dev (>= 1.5-~~), librust-serde-1+default-dev, librust-serde-json-1+default-dev +Recommends: + librust-proxmox-subscription+impl-dev (= ${binary:Version}) Suggests: librust-proxmox-subscription+api-types-dev (= ${binary:Version}) Provides: - librust-proxmox-subscription+default-dev (= ${binary:Version}), librust-proxmox-subscription-0-dev (= ${binary:Version}), - librust-proxmox-subscription-0+default-dev (= ${binary:Version}), - librust-proxmox-subscription-0.4-dev (= ${binary:Version}), - librust-proxmox-subscription-0.4+default-dev (= ${binary:Version}), - librust-proxmox-subscription-0.4.6-dev (= ${binary:Version}), - librust-proxmox-subscription-0.4.6+default-dev (= ${binary:Version}) + librust-proxmox-subscription-0.5-dev (= ${binary:Version}), + librust-proxmox-subscription-0.5.0-dev (= ${binary:Version}) Description: Proxmox subscription utilitites - Rust source code Source code for Debianized Rust crate "proxmox-subscription" @@ -69,8 +60,36 @@ Depends: librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) Provides: librust-proxmox-subscription-0+api-types-dev (= ${binary:Version}), - librust-proxmox-subscription-0.4+api-types-dev (= ${binary:Version}), - librust-proxmox-subscription-0.4.6+api-types-dev (= ${binary:Version}) + librust-proxmox-subscription-0.5+api-types-dev (= ${binary:Version}), + librust-proxmox-subscription-0.5.0+api-types-dev (= ${binary:Version}) Description: Proxmox subscription utilitites - feature "api-types" This metapackage enables feature "api-types" for the Rust proxmox-subscription crate, by pulling in any additional dependencies needed by that feature. + +Package: librust-proxmox-subscription+impl-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-subscription-dev (= ${binary:Version}), + librust-base64-0.13+default-dev, + librust-hex-0.4+default-dev, + librust-openssl-0.10+default-dev, + librust-proxmox-http-0.9+client-trait-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~), + librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.4-~~), + librust-proxmox-sys-0.6+default-dev, + librust-proxmox-time-2+default-dev +Provides: + librust-proxmox-subscription+default-dev (= ${binary:Version}), + librust-proxmox-subscription-0+impl-dev (= ${binary:Version}), + librust-proxmox-subscription-0+default-dev (= ${binary:Version}), + librust-proxmox-subscription-0.5+impl-dev (= ${binary:Version}), + librust-proxmox-subscription-0.5+default-dev (= ${binary:Version}), + librust-proxmox-subscription-0.5.0+impl-dev (= ${binary:Version}), + librust-proxmox-subscription-0.5.0+default-dev (= ${binary:Version}) +Description: Proxmox subscription utilitites - feature "impl" and 1 more + This metapackage enables feature "impl" for the Rust proxmox-subscription + crate, by pulling in any additional dependencies needed by that feature. + . + Additionally, this package also provides the "default" feature. diff --git a/proxmox-subscription/src/lib.rs b/proxmox-subscription/src/lib.rs index bcc10721..2ed96903 100644 --- a/proxmox-subscription/src/lib.rs +++ b/proxmox-subscription/src/lib.rs @@ -1,10 +1,17 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] mod subscription_info; +#[cfg(feature = "impl")] pub use subscription_info::{ get_hardware_address, ProductType, SubscriptionInfo, SubscriptionStatus, }; +#[cfg(not(feature = "impl"))] +pub use subscription_info::{ProductType, SubscriptionInfo, SubscriptionStatus}; + +#[cfg(feature = "impl")] pub mod check; +#[cfg(feature = "impl")] pub mod files; +#[cfg(feature = "impl")] pub mod sign; diff --git a/proxmox-subscription/src/subscription_info.rs b/proxmox-subscription/src/subscription_info.rs index a86572d0..f53b3ce3 100644 --- a/proxmox-subscription/src/subscription_info.rs +++ b/proxmox-subscription/src/subscription_info.rs @@ -1,23 +1,11 @@ -use std::{fmt::Display, path::Path, str::FromStr}; +use std::{fmt::Display, str::FromStr}; -use anyhow::{bail, format_err, Error}; -use openssl::hash::{hash, DigestBytes, MessageDigest}; -use proxmox_sys::fs::file_get_contents; -use proxmox_time::TmEditor; +use anyhow::{bail, Error}; use serde::{Deserialize, Serialize}; #[cfg(feature = "api-types")] use proxmox_schema::{api, Updater}; -use crate::sign::Verifier; - -pub(crate) const SHARED_KEY_DATA: &str = "kjfdlskfhiuewhfk947368"; - -/// How long the local key is valid for in between remote checks -pub(crate) const SUBSCRIPTION_MAX_LOCAL_KEY_AGE: i64 = 15 * 24 * 3600; -pub(crate) const SUBSCRIPTION_MAX_LOCAL_SIGNED_KEY_AGE: i64 = 365 * 24 * 3600; -pub(crate) const SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE: i64 = 5 * 24 * 3600; - // Aliases are needed for PVE compat! #[cfg_attr(feature = "api-types", api())] #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -144,196 +132,226 @@ pub struct SubscriptionInfo { pub signature: Option, } -impl SubscriptionInfo { - /// Returns the canonicalized signed data and, if available, signature contained in `self`. - pub fn signed_data(&self) -> Result<(Vec, Option), Error> { - let mut data = serde_json::to_value(self)?; - let signature = data - .as_object_mut() - .ok_or_else(|| format_err!("subscription info not a JSON object"))? - .remove("signature") - .and_then(|v| v.as_str().map(|v| v.to_owned())); +#[cfg(feature = "impl")] +pub use _impl::get_hardware_address; - if self.is_signed() && signature.is_none() { - bail!("Failed to extract signature value!"); - } +#[cfg(feature = "impl")] +pub(crate) use _impl::{md5sum, SHARED_KEY_DATA}; - let data = proxmox_serde::json::to_canonical_json(&data)?; - Ok((data, signature)) - } +#[cfg(feature = "impl")] +mod _impl { - /// Whether a signature exists - *this does not check the signature's validity!* - /// - /// Use [SubscriptionInfo::check_signature()] to verify the - /// signature. - pub fn is_signed(&self) -> bool { - self.signature.is_some() - } + use std::path::Path; - /// Checks whether a [SubscriptionInfo]'s `checktime` matches the age criteria: - /// - /// - Instances generated (more than 1.5h) in the future are invalid - /// - Signed instances are valid for up to a year, clamped by the next due date - /// - Unsigned instances are valid for 30+5 days - /// - If `recheck` is set to `true`, unsigned instances are only treated as valid for 5 days - /// (this mode is used to decide whether to refresh the subscription information) - /// - /// If the criteria are not met, `status` is set to [SubscriptionStatus::Invalid] and `message` - /// to a human-readable error message. - pub fn check_age(&mut self, recheck: bool) { - let now = proxmox_time::epoch_i64(); - let age = now - self.checktime.unwrap_or(0); + use anyhow::format_err; + use anyhow::{bail, Error}; + use openssl::hash::{hash, DigestBytes, MessageDigest}; + use proxmox_sys::fs::file_get_contents; + use proxmox_time::TmEditor; - let cutoff = if self.is_signed() { - SUBSCRIPTION_MAX_LOCAL_SIGNED_KEY_AGE - } else if recheck { - SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE - } else { - SUBSCRIPTION_MAX_LOCAL_KEY_AGE + SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE - }; + use crate::sign::Verifier; - // allow some delta for DST changes or time syncs, 1.5h - if age < -5400 { - self.status = SubscriptionStatus::Invalid; - self.message = Some("last check date too far in the future".to_string()); - self.signature = None; - } else if age > cutoff { - if let SubscriptionStatus::Active = self.status { - self.status = SubscriptionStatus::Invalid; - self.message = Some("subscription information too old".to_string()); - self.signature = None; + pub(crate) const SHARED_KEY_DATA: &str = "kjfdlskfhiuewhfk947368"; + + /// How long the local key is valid for in between remote checks + pub(crate) const SUBSCRIPTION_MAX_LOCAL_KEY_AGE: i64 = 15 * 24 * 3600; + pub(crate) const SUBSCRIPTION_MAX_LOCAL_SIGNED_KEY_AGE: i64 = 365 * 24 * 3600; + pub(crate) const SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE: i64 = 5 * 24 * 3600; + + use super::{ProductType, SubscriptionInfo, SubscriptionStatus}; + + impl SubscriptionInfo { + /// Returns the canonicalized signed data and, if available, signature contained in `self`. + pub fn signed_data(&self) -> Result<(Vec, Option), Error> { + let mut data = serde_json::to_value(self)?; + let signature = data + .as_object_mut() + .ok_or_else(|| format_err!("subscription info not a JSON object"))? + .remove("signature") + .and_then(|v| v.as_str().map(|v| v.to_owned())); + + if self.is_signed() && signature.is_none() { + bail!("Failed to extract signature value!"); } + + let data = proxmox_serde::json::to_canonical_json(&data)?; + Ok((data, signature)) } - if self.is_signed() && self.status == SubscriptionStatus::Active { - if let Some(next_due) = self.nextduedate.as_ref() { - match parse_next_due(next_due.as_str()) { - Ok(next_due) if now > next_due => { - self.status = SubscriptionStatus::Invalid; - self.message = Some("subscription information too old".to_string()); - self.signature = None; - } - Ok(_) => {} - Err(err) => { - self.status = SubscriptionStatus::Invalid; - self.message = Some(format!("Failed parsing 'nextduedate' - {err}")); - self.signature = None; + /// Whether a signature exists - *this does not check the signature's validity!* + /// + /// Use [SubscriptionInfo::check_signature()] to verify the + /// signature. + pub fn is_signed(&self) -> bool { + self.signature.is_some() + } + + /// Checks whether a [SubscriptionInfo]'s `checktime` matches the age criteria: + /// + /// - Instances generated (more than 1.5h) in the future are invalid + /// - Signed instances are valid for up to a year, clamped by the next due date + /// - Unsigned instances are valid for 30+5 days + /// - If `recheck` is set to `true`, unsigned instances are only treated as valid for 5 days + /// (this mode is used to decide whether to refresh the subscription information) + /// + /// If the criteria are not met, `status` is set to [SubscriptionStatus::Invalid] and `message` + /// to a human-readable error message. + pub fn check_age(&mut self, recheck: bool) { + let now = proxmox_time::epoch_i64(); + let age = now - self.checktime.unwrap_or(0); + + let cutoff = if self.is_signed() { + SUBSCRIPTION_MAX_LOCAL_SIGNED_KEY_AGE + } else if recheck { + SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE + } else { + SUBSCRIPTION_MAX_LOCAL_KEY_AGE + SUBSCRIPTION_MAX_KEY_CHECK_FAILURE_AGE + }; + + // allow some delta for DST changes or time syncs, 1.5h + if age < -5400 { + self.status = SubscriptionStatus::Invalid; + self.message = Some("last check date too far in the future".to_string()); + self.signature = None; + } else if age > cutoff { + if let SubscriptionStatus::Active = self.status { + self.status = SubscriptionStatus::Invalid; + self.message = Some("subscription information too old".to_string()); + self.signature = None; + } + } + + if self.is_signed() && self.status == SubscriptionStatus::Active { + if let Some(next_due) = self.nextduedate.as_ref() { + match parse_next_due(next_due.as_str()) { + Ok(next_due) if now > next_due => { + self.status = SubscriptionStatus::Invalid; + self.message = Some("subscription information too old".to_string()); + self.signature = None; + } + Ok(_) => {} + Err(err) => { + self.status = SubscriptionStatus::Invalid; + self.message = Some(format!("Failed parsing 'nextduedate' - {err}")); + self.signature = None; + } } } } } - } - /// Check that server ID contained in [SubscriptionInfo] matches that of current system. - /// - /// `status` is set to [SubscriptionStatus::Invalid] and `message` to a human-readable - /// message in case it does not. - pub fn check_server_id(&mut self) { - match (self.serverid.as_ref(), get_hardware_address()) { - (_, Err(err)) => { - self.status = SubscriptionStatus::Invalid; - self.message = Some(format!("Failed to obtain server ID - {err}.")); - self.signature = None; + /// Check that server ID contained in [SubscriptionInfo] matches that of current system. + /// + /// `status` is set to [SubscriptionStatus::Invalid] and `message` to a human-readable + /// message in case it does not. + pub fn check_server_id(&mut self) { + match (self.serverid.as_ref(), get_hardware_address()) { + (_, Err(err)) => { + self.status = SubscriptionStatus::Invalid; + self.message = Some(format!("Failed to obtain server ID - {err}.")); + self.signature = None; + } + (None, _) => { + self.status = SubscriptionStatus::Invalid; + self.message = Some("Missing server ID.".to_string()); + self.signature = None; + } + (Some(contained), Ok(expected)) if &expected != contained => { + self.status = SubscriptionStatus::Invalid; + self.message = Some("Server ID mismatch.".to_string()); + self.signature = None; + } + (Some(_), Ok(_)) => {} } - (None, _) => { - self.status = SubscriptionStatus::Invalid; - self.message = Some("Missing server ID.".to_string()); - self.signature = None; - } - (Some(contained), Ok(expected)) if &expected != contained => { - self.status = SubscriptionStatus::Invalid; - self.message = Some("Server ID mismatch.".to_string()); - self.signature = None; - } - (Some(_), Ok(_)) => {} } - } - /// Check a [SubscriptionInfo]'s signature, if one is available. - /// - /// `status` is set to [SubscriptionStatus::Invalid] and `message` to a human-readable error - /// message in case a signature is available but not valid for the given `key`. - pub fn check_signature>(&mut self, keys: &[P]) { - let verify = |info: &SubscriptionInfo, path: &P| -> Result<(), Error> { - let raw = file_get_contents(path)?; + /// Check a [SubscriptionInfo]'s signature, if one is available. + /// + /// `status` is set to [SubscriptionStatus::Invalid] and `message` to a human-readable error + /// message in case a signature is available but not valid for the given `key`. + pub fn check_signature>(&mut self, keys: &[P]) { + let verify = |info: &SubscriptionInfo, path: &P| -> Result<(), Error> { + let raw = file_get_contents(path)?; - let key = openssl::pkey::PKey::public_key_from_pem(&raw)?; + let key = openssl::pkey::PKey::public_key_from_pem(&raw)?; - let (signed, signature) = info.signed_data()?; - let signature = match signature { - None => bail!("Failed to extract signature value."), - Some(sig) => sig, + let (signed, signature) = info.signed_data()?; + let signature = match signature { + None => bail!("Failed to extract signature value."), + Some(sig) => sig, + }; + + key.verify(&signed, &signature) + .map_err(|err| format_err!("Signature verification failed - {err}")) }; - key.verify(&signed, &signature) - .map_err(|err| format_err!("Signature verification failed - {err}")) - }; - - if self.is_signed() { - if keys.is_empty() { - self.status = SubscriptionStatus::Invalid; - self.message = Some("Signature exists, but no key available.".to_string()); - } else if !keys.iter().any(|key| verify(self, key).is_ok()) { - self.status = SubscriptionStatus::Invalid; - self.message = Some("Signature validation failed".to_string()); + if self.is_signed() { + if keys.is_empty() { + self.status = SubscriptionStatus::Invalid; + self.message = Some("Signature exists, but no key available.".to_string()); + } else if !keys.iter().any(|key| verify(self, key).is_ok()) { + self.status = SubscriptionStatus::Invalid; + self.message = Some("Signature validation failed".to_string()); + } } } + + pub fn get_product_type(&self) -> Result { + self.key + .as_ref() + .ok_or_else(|| format_err!("no product key set")) + .map(|key| key[..3].parse::())? + } + + pub fn get_next_due_date(&self) -> Result { + self.nextduedate + .as_ref() + .ok_or_else(|| format_err!("no next due date set")) + .map(|e| parse_next_due(e))? + } } - pub fn get_product_type(&self) -> Result { - self.key - .as_ref() - .ok_or_else(|| format_err!("no product key set")) - .map(|key| key[..3].parse::())? + /// Shortcut for md5 sums. + pub(crate) fn md5sum(data: &[u8]) -> Result { + hash(MessageDigest::md5(), data).map_err(Error::from) } - pub fn get_next_due_date(&self) -> Result { - self.nextduedate - .as_ref() - .ok_or_else(|| format_err!("no next due date set")) - .map(|e| parse_next_due(e))? - } -} + /// Generate the current system's "server ID". + pub fn get_hardware_address() -> Result { + static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub"; -/// Shortcut for md5 sums. -pub(crate) fn md5sum(data: &[u8]) -> Result { - hash(MessageDigest::md5(), data).map_err(Error::from) -} + let contents = proxmox_sys::fs::file_get_contents(FILENAME) + .map_err(|e| format_err!("Error getting host key - {}", e))?; + let digest = + md5sum(&contents).map_err(|e| format_err!("Error digesting host key - {}", e))?; -/// Generate the current system's "server ID". -pub fn get_hardware_address() -> Result { - static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub"; - - let contents = proxmox_sys::fs::file_get_contents(FILENAME) - .map_err(|e| format_err!("Error getting host key - {}", e))?; - let digest = md5sum(&contents).map_err(|e| format_err!("Error digesting host key - {}", e))?; - - Ok(hex::encode(digest).to_uppercase()) -} - -fn parse_next_due(value: &str) -> Result { - let mut components = value.split('-'); - let year = components - .next() - .ok_or_else(|| format_err!("missing year component."))? - .parse::()?; - let month = components - .next() - .ok_or_else(|| format_err!("missing month component."))? - .parse::()?; - let day = components - .next() - .ok_or_else(|| format_err!("missing day component."))? - .parse::()?; - - if components.next().is_some() { - bail!("cannot parse 'nextduedate' value '{value}'"); + Ok(hex::encode(digest).to_uppercase()) } - let mut tm = TmEditor::new(true); - tm.set_year(year)?; - tm.set_mon(month)?; - tm.set_mday(day)?; + fn parse_next_due(value: &str) -> Result { + let mut components = value.split('-'); + let year = components + .next() + .ok_or_else(|| format_err!("missing year component."))? + .parse::()?; + let month = components + .next() + .ok_or_else(|| format_err!("missing month component."))? + .parse::()?; + let day = components + .next() + .ok_or_else(|| format_err!("missing day component."))? + .parse::()?; - tm.into_epoch() + if components.next().is_some() { + bail!("cannot parse 'nextduedate' value '{value}'"); + } + + let mut tm = TmEditor::new(true); + tm.set_year(year)?; + tm.set_mon(month)?; + tm.set_mday(day)?; + + tm.into_epoch() + } } diff --git a/proxmox-sys/debian/control b/proxmox-sys/debian/control index 1387a65a..7651fad0 100644 --- a/proxmox-sys/debian/control +++ b/proxmox-sys/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-sys Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native (>= 1.80) , libstd-rust-dev , @@ -19,9 +19,10 @@ Build-Depends: debhelper (>= 12), libacl1-dev , uuid-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-sys Rules-Requires-Root: no diff --git a/proxmox-sys/src/fd.rs b/proxmox-sys/src/fd.rs index 8d85bd2e..386e4222 100644 --- a/proxmox-sys/src/fd.rs +++ b/proxmox-sys/src/fd.rs @@ -24,7 +24,7 @@ pub fn change_cloexec(fd: RawFd, on: bool) -> Result<(), anyhow::Error> { } pub(crate) fn cwd() -> Result { - open(".", OFlag::O_DIRECTORY, stat::Mode::empty()) + open(".", crate::fs::DIR_FLAGS, stat::Mode::empty()) } pub fn open

(path: &P, oflag: OFlag, mode: Mode) -> Result diff --git a/proxmox-sys/src/fs/dir.rs b/proxmox-sys/src/fs/dir.rs index 4db63409..a982866c 100644 --- a/proxmox-sys/src/fs/dir.rs +++ b/proxmox-sys/src/fs/dir.rs @@ -14,6 +14,11 @@ use proxmox_lang::try_block; use crate::fs::{fchown, CreateOptions}; +/// The default list of [`OFlag`]'s we want to use when opening directories. Besides ensuring that +/// the FD indeed points to a directory we also must ensure that it gets closed on exec to avoid +/// leaking a open FD to a child process. +pub(crate) const DIR_FLAGS: OFlag = OFlag::O_DIRECTORY.union(OFlag::O_CLOEXEC); + /// Creates directory at the provided path with specified ownership. /// /// Errors if the directory already exists. @@ -66,7 +71,7 @@ pub fn ensure_dir_exists>( Err(err) => bail!("unable to create directory {path:?} - {err}",), } - let fd = nix::fcntl::open(path, OFlag::O_DIRECTORY, stat::Mode::empty()) + let fd = nix::fcntl::open(path, DIR_FLAGS, stat::Mode::empty()) .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) .map_err(|err| format_err!("unable to open created directory {path:?} - {err}"))?; // umask defaults to 022 so make sure the mode is fully honowed: @@ -120,7 +125,7 @@ fn create_path_do( Some(Component::Prefix(_)) => bail!("illegal prefix path component encountered"), Some(Component::RootDir) => { let _ = iter.next(); - crate::fd::open(c"/", OFlag::O_DIRECTORY, stat::Mode::empty())? + crate::fd::open(c"/", DIR_FLAGS, stat::Mode::empty())? } Some(Component::CurDir) => { let _ = iter.next(); @@ -128,7 +133,7 @@ fn create_path_do( } Some(Component::ParentDir) => { let _ = iter.next(); - crate::fd::open(c"..", OFlag::O_DIRECTORY, stat::Mode::empty())? + crate::fd::open(c"..", DIR_FLAGS, stat::Mode::empty())? } Some(Component::Normal(_)) => { // simply do not advance the iterator, heavy lifting happens in create_path_at_do() @@ -154,7 +159,7 @@ fn create_path_at_do( None => return Ok(created), Some(Component::ParentDir) => { - at = crate::fd::openat(&at, c"..", OFlag::O_DIRECTORY, stat::Mode::empty())?; + at = crate::fd::openat(&at, c"..", DIR_FLAGS, stat::Mode::empty())?; } Some(Component::Normal(path)) => { @@ -175,7 +180,7 @@ fn create_path_at_do( Err(e) => return Err(e.into()), Ok(_) => true, }; - at = crate::fd::openat(&at, path, OFlag::O_DIRECTORY, stat::Mode::empty())?; + at = crate::fd::openat(&at, path, DIR_FLAGS, stat::Mode::empty())?; if let (true, Some(opts)) = (created, opts) { if opts.owner.is_some() || opts.group.is_some() { @@ -222,7 +227,7 @@ pub fn make_tmp_dir>( if let Some(options) = options { if let Err(err) = try_block!({ - let mut fd = crate::fd::open(&path, OFlag::O_DIRECTORY, stat::Mode::empty())?; + let mut fd = crate::fd::open(&path, DIR_FLAGS, stat::Mode::empty())?; options.apply_to(&mut fd, &path)?; Ok::<(), Error>(()) }) { diff --git a/proxmox-sys/src/fs/file.rs b/proxmox-sys/src/fs/file.rs index fbfc0b58..74b9e74e 100644 --- a/proxmox-sys/src/fs/file.rs +++ b/proxmox-sys/src/fs/file.rs @@ -116,6 +116,29 @@ pub fn file_read_firstline>(path: P) -> Result { read_firstline(path).map_err(|err| format_err!("unable to read {path:?} - {err}")) } +#[inline] +/// Creates a tmpfile like [`nix::unistd::mkstemp`], but with [`nix::fctnl::Oflag`] set. +/// +/// Note that some flags are masked out since they can produce an error, see mkostemp(2) for details. +// code is mostly copied from nix mkstemp +fn mkostemp( + template: &P, + oflag: OFlag, +) -> nix::Result<(std::os::fd::RawFd, PathBuf)> { + use std::os::unix::ffi::OsStringExt; + let mut path = template.with_nix_path(|path| path.to_bytes_with_nul().to_owned())?; + let p = path.as_mut_ptr().cast(); + + let flags = OFlag::intersection(OFlag::O_APPEND | OFlag::O_CLOEXEC | OFlag::O_SYNC, oflag); + + let fd = unsafe { libc::mkostemp(p, flags.bits()) }; + let last = path.pop(); // drop the trailing nul + debug_assert!(last == Some(b'\0')); + let pathname = std::ffi::OsString::from_vec(path); + Errno::result(fd)?; + Ok((fd, PathBuf::from(pathname))) +} + /// Takes a Path and CreateOptions, creates a tmpfile from it and returns /// a RawFd and PathBuf for it pub fn make_tmp_file>( @@ -127,7 +150,7 @@ pub fn make_tmp_file>( // use mkstemp here, because it works with different processes, threads, even tokio tasks let mut template = path.to_owned(); template.set_extension("tmp_XXXXXX"); - let (mut file, tmp_path) = match unistd::mkstemp(&template) { + let (mut file, tmp_path) = match mkostemp(&template, OFlag::O_CLOEXEC) { Ok((fd, path)) => (unsafe { File::from_raw_fd(fd) }, path), Err(err) => bail!("mkstemp {:?} failed: {}", template, err), }; diff --git a/proxmox-syslog-api/debian/control b/proxmox-syslog-api/debian/control index 1ff9bccf..aad4a70b 100644 --- a/proxmox-syslog-api/debian/control +++ b/proxmox-syslog-api/debian/control @@ -1,10 +1,10 @@ Source: rust-proxmox-syslog-api Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , - rustc:native , + rustc:native (>= 1.80) , libstd-rust-dev , librust-anyhow-1+default-dev , librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) , @@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-1+derive-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-syslog-api Rules-Requires-Root: no diff --git a/proxmox-systemd/debian/control b/proxmox-systemd/debian/control index f27a8519..18efc540 100644 --- a/proxmox-systemd/debian/control +++ b/proxmox-systemd/debian/control @@ -1,17 +1,18 @@ Source: rust-proxmox-systemd Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , librust-libc-0.2+default-dev (>= 0.2.107-~~) , libsystemd-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-systemd Rules-Requires-Root: no diff --git a/proxmox-tfa/debian/control b/proxmox-tfa/debian/control index 29aeacc8..83611dc6 100644 --- a/proxmox-tfa/debian/control +++ b/proxmox-tfa/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-tfa Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-plain-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git Homepage: https://proxmox.com diff --git a/proxmox-time-api/debian/control b/proxmox-time-api/debian/control index 5f96087c..991ddca1 100644 --- a/proxmox-time-api/debian/control +++ b/proxmox-time-api/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-time-api Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , @@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+default-dev , librust-serde-1+derive-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-time-api Rules-Requires-Root: no diff --git a/proxmox-time/Cargo.toml b/proxmox-time/Cargo.toml index 9989bb1d..db984e74 100644 --- a/proxmox-time/Cargo.toml +++ b/proxmox-time/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "proxmox-time" description = "time utilities and TmEditor" -version = "2.0.2" +version = "2.0.3" authors.workspace = true edition.workspace = true diff --git a/proxmox-time/debian/changelog b/proxmox-time/debian/changelog index 737e5872..85ccbde4 100644 --- a/proxmox-time/debian/changelog +++ b/proxmox-time/debian/changelog @@ -1,3 +1,10 @@ +rust-proxmox-time (2.0.3-1) bookworm; urgency=medium + + * also implement `From<&TimeSpan> for f64` to support taking a reference, as + we do not require ownership here. + + -- Proxmox Support Team Tue, 26 Nov 2024 16:52:28 +0100 + rust-proxmox-time (2.0.2-1) bookworm; urgency=medium * time span: diff --git a/proxmox-time/debian/control b/proxmox-time/debian/control index 0a408745..ec32c91a 100644 --- a/proxmox-time/debian/control +++ b/proxmox-time/debian/control @@ -37,7 +37,7 @@ Provides: librust-proxmox-time-2+default-dev (= ${binary:Version}), librust-proxmox-time-2.0-dev (= ${binary:Version}), librust-proxmox-time-2.0+default-dev (= ${binary:Version}), - librust-proxmox-time-2.0.2-dev (= ${binary:Version}), - librust-proxmox-time-2.0.2+default-dev (= ${binary:Version}) + librust-proxmox-time-2.0.3-dev (= ${binary:Version}), + librust-proxmox-time-2.0.3+default-dev (= ${binary:Version}) Description: Time utilities and TmEditor - Rust source code Source code for Debianized Rust crate "proxmox-time" diff --git a/proxmox-time/src/time_span.rs b/proxmox-time/src/time_span.rs index a1bbbca9..6081a534 100644 --- a/proxmox-time/src/time_span.rs +++ b/proxmox-time/src/time_span.rs @@ -1,14 +1,14 @@ //! Timespans that try to be compatible with the systemd time span format. //! //! Time spans refer to time durations, like [std::time::Duration] but in the format that is -//! targetting human interfaces and that systemd understands. Parts of this documentation have been +//! targeting human interfaces and that systemd understands. Parts of this documentation have been //! adapted from the systemd.time manual page. //! //! The following time units are understood: //! - `nsec`, `ns` (not always accepted by systemd.time) //! - `usec`, `us`, `µs` //! - `msec`, `ms` -//! - `seconds`, s`econd`, `sec`, `s` +//! - `seconds`, `second`, `sec`, `s` //! - `minutes`, `minute`, `min`, `m` //! - `hours`, `hour`, `hr`, `h` //! - `days`, `day`, `d` @@ -26,7 +26,7 @@ //! spaces between units and/or values can be added or omitted. The order of the time values does //! not matter. //! -//! The following examples are all represeting the exact same time span of 1 day 2 hours and 3 +//! The following examples are all representing the exact same time span of 1 day 2 hours and 3 //! minutes: //! //! - `1d 2h 3m` @@ -128,8 +128,8 @@ pub struct TimeSpan { pub years: u64, } -impl From for f64 { - fn from(ts: TimeSpan) -> Self { +impl From<&TimeSpan> for f64 { + fn from(ts: &TimeSpan) -> Self { (ts.seconds as f64) + ((ts.nsec as f64) / 1_000_000_000.0) + ((ts.usec as f64) / 1_000_000.0) @@ -143,6 +143,12 @@ impl From for f64 { } } +impl From for f64 { + fn from(ts: TimeSpan) -> Self { + Self::from(&ts) + } +} + impl From for TimeSpan { fn from(duration: std::time::Duration) -> Self { let mut duration = duration.as_nanos(); @@ -306,8 +312,8 @@ pub fn verify_time_span(i: &str) -> Result<(), Error> { #[cfg(test)] mod tests { - use std::str::FromStr; use super::*; + use std::str::FromStr; #[test] fn conversions() { diff --git a/proxmox-uuid/debian/control b/proxmox-uuid/debian/control index 1aa707cd..0399bb01 100644 --- a/proxmox-uuid/debian/control +++ b/proxmox-uuid/debian/control @@ -1,17 +1,18 @@ Source: rust-proxmox-uuid Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , librust-js-sys-0.3+default-dev (>= 0.3.55-~~) , uuid-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-uuid Rules-Requires-Root: no diff --git a/proxmox-worker-task/debian/control b/proxmox-worker-task/debian/control index f10a0500..1b01d645 100644 --- a/proxmox-worker-task/debian/control +++ b/proxmox-worker-task/debian/control @@ -1,16 +1,17 @@ Source: rust-proxmox-worker-task Section: rust Priority: optional -Build-Depends: debhelper (>= 12), - dh-cargo (>= 25), +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo, cargo:native , rustc:native , libstd-rust-dev , librust-anyhow-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.6.2 +Standards-Version: 4.7.0 Vcs-Git: git://git.proxmox.com/git/proxmox.git Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-worker-task Rules-Requires-Root: no