Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Сергей Конев 2024-12-02 20:25:20 +03:00
commit 491814c1cd
100 changed files with 2937 additions and 697 deletions

View File

@ -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" }

View File

@ -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/$*

View File

@ -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.

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-time-2+default-dev <!nocheck>,
librust-serde-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -17,9 +17,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -14,7 +14,7 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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}),

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -14,7 +14,7 @@ Build-Depends: debhelper (>= 12),
librust-syn-2+full-dev <!nocheck>,
librust-syn-2+visit-mut-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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

View File

@ -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 <support@proxmox.com> 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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-proxmox-config-digest-0.1+default-dev <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-schema-3+default-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~) <!nocheck>,
librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
librust-serde-plain-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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,

View File

@ -1,7 +1,7 @@
[package]
name = "proxmox-apt"
description = "Proxmox library for APT"
version = "0.11.3"
version = "0.11.5"
exclude = ["debian"]

View File

@ -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 <support@proxmox.com> 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 <support@proxmox.com> Sun, 10 Nov 2024 18:48:37 +0100
rust-proxmox-apt (0.11.3-1) bookworm; urgency=medium
* drop unused dependency on once_cell

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-hex-0.4+default-dev <!nocheck>,
librust-openssl-0.10+default-dev <!nocheck>,
librust-proxmox-apt-api-types-1+default-dev (>= 1.0.1-~~) <!nocheck>,
librust-proxmox-apt-api-types-1+default-dev (>= 1.0.2-~~) <!nocheck>,
librust-proxmox-config-digest-0.1+default-dev <!nocheck>,
librust-proxmox-config-digest-0.1+openssl-dev <!nocheck>,
librust-proxmox-sys-0.6+default-dev <!nocheck>,
@ -18,7 +18,7 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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.

View File

@ -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),
]);
}
}

View File

@ -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()],

View File

@ -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");

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-futures-0.3+default-dev <!nocheck>,
@ -20,9 +20,10 @@ Build-Depends: debhelper (>= 12),
libssl-dev <!nocheck>,
uuid-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -14,9 +14,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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}),

View File

@ -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"));
}

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -25,9 +25,10 @@ Build-Depends: debhelper (>= 12),
librust-zstd-0.12+bindgen-dev <!nocheck>,
librust-zstd-0.12+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-plain-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -22,9 +22,10 @@ Build-Depends: debhelper (>= 12),
librust-tokio-1+signal-dev (>= 1.6-~~) <!nocheck>,
librust-tokio-1+sync-dev (>= 1.6-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -16,9 +16,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-http-0.2+default-dev <!nocheck>,
librust-serde-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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

View File

@ -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 <support@proxmox.com> Tue, 26 Nov 2024 14:00:00 +0100
rust-proxmox-http (0.9.3-1) bookworm; urgency=medium
* rebuild with proxmox-sys 6.0

View File

@ -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.

View File

@ -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<Duration>,
}
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<ureq::Agent, Error> {
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);

View File

@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-endian-trait-0.6+arrays-dev <!nocheck>,
librust-endian-trait-0.6+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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

View File

@ -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 <support@proxmox.com> 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 <support@proxmox.com> 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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-nix-0.26+default-dev (>= 0.26.1-~~) <!nocheck>,
@ -17,9 +17,10 @@ Build-Depends: debhelper (>= 12),
librust-tracing-log-0.2+std-dev <!nocheck>,
librust-tracing-subscriber-0.3+default-dev (>= 0.3.16-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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<S: AsRef<str>>(&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<usize, std::io::Error> {
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()
}
}

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-futures-0.3+default-dev <!nocheck>,
@ -12,8 +12,8 @@ Build-Depends: debhelper (>= 12),
librust-hyper-0.14+default-dev (>= 0.14.5-~~) <!nocheck>,
librust-openssl-0.10+default-dev <!nocheck>,
librust-proxmox-async-0.4+default-dev (>= 0.4.1-~~) <!nocheck>,
librust-proxmox-http-0.9+client-dev <!nocheck>,
librust-proxmox-http-0.9+default-dev <!nocheck>,
librust-proxmox-http-0.9+client-dev (>= 0.9.4-~~) <!nocheck>,
librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>,
librust-tokio-1+default-dev (>= 1.6-~~) <!nocheck>,
@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12),
librust-tokio-1+sync-dev (>= 1.6-~~) <!nocheck>,
librust-url-2+default-dev (>= 2.2-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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-~~),

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -15,9 +15,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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"]

View File

@ -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 <support@proxmox.com> 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 <support@proxmox.com> Sun, 10 Nov 2024 18:57:36 +0100
rust-proxmox-notify (0.4.2-1) bookworm; urgency=medium
* upgrade to proxmox-sys 6.0

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-base64-0.13+default-dev <!nocheck>,
librust-const-format-0.2+default-dev <!nocheck>,
librust-handlebars-3+default-dev <!nocheck>,
librust-http-0.2+default-dev <!nocheck>,
librust-lettre-0.11+default-dev (>= 0.11.1-~~) <!nocheck>,
librust-log-0.4+default-dev (>= 0.4.17-~~) <!nocheck>,
librust-openssl-0.10+default-dev <!nocheck>,
librust-proxmox-http-0.9+client-sync-dev (>= 0.9.2-~~) <!nocheck>,
librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) <!nocheck>,
librust-percent-encoding-2+default-dev (>= 2.1-~~) <!nocheck>,
librust-proxmox-http-0.9+client-sync-dev (>= 0.9.4-~~) <!nocheck>,
librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) <!nocheck>,
librust-proxmox-http-error-0.1+default-dev <!nocheck>,
librust-proxmox-human-byte-0.1+default-dev <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-schema-3+api-types-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-schema-3+default-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-section-config-2+default-dev <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~) <!nocheck>,
librust-proxmox-schema-3+api-types-dev (>= 3.1.2-~~) <!nocheck>,
librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) <!nocheck>,
librust-proxmox-section-config-2+default-dev (>= 2.1.0-~~) <!nocheck>,
librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-sys-0.6+default-dev <!nocheck>,
@ -32,9 +34,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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.

View File

@ -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<Vec<Target>, 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")

View File

@ -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<Vec<WebhookConfig>, HttpError> {
let mut endpoints: Vec<WebhookConfig> = 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<WebhookConfig, HttpError> {
let mut endpoint: WebhookConfig = config
.config
.lookup(WEBHOOK_TYPENAME, name)
.map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))?;
let priv_config: Option<WebhookPrivateConfig> = 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::<WebhookPrivateConfig>(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<PropertyString<KeyAndBase64Val>> = 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::<WebhookPrivateConfig>(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::<WebhookPrivateConfig>(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(())
}
}

View File

@ -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
}

View File

@ -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

View File

@ -4,5 +4,7 @@ pub mod gotify;
pub mod sendmail;
#[cfg(feature = "smtp")]
pub mod smtp;
#[cfg(feature = "webhook")]
pub mod webhook;
mod common;

View File

@ -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<u32>) -> 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("<b>HTML</b>"),
None,
Some("test1"),
);
assert!(result.is_err());
}
}

View File

@ -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.<name> }}`.
//! 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<HttpMethod> 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<PropertyString<KeyAndBase64Val>>,
/// The HTTP body to send. Supports templating.
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
/// Comment.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
/// Disable this target.
#[serde(skip_serializing_if = "Option::is_none")]
pub disable: Option<bool>,
/// Origin of this config entry.
#[serde(skip_serializing_if = "Option::is_none")]
#[updater(skip)]
pub origin: Option<Origin>,
/// 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<PropertyString<KeyAndBase64Val>>,
}
#[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<PropertyString<KeyAndBase64Val>>,
}
/// 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<String>,
}
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<String, Error> {
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<Client, Error> {
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<Request<String>, Error> {
let (title, message) = match &notification.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 &notification.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<String, Error> {
// 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, "<masked>"),
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(&notification)?;
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(())
}
}

View File

@ -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)

View File

@ -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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-http-0.2+default-dev <!nocheck>,
@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12),
librust-ureq-2+gzip-dev (>= 2.4-~~) <!nocheck>,
librust-ureq-2+native-tls-dev (>= 2.4-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -11,9 +11,10 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-sys-0.6+default-dev <!nocheck>,
librust-proxmox-sys-0.6+timer-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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

View File

@ -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 <support@proxmox.com> 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 <support@proxmox.com> 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 <support@proxmox.com> 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 <support@proxmox.com> 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

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -20,7 +20,7 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-compression-0.2+default-dev (>= 0.2.4-~~) <!nocheck>,
librust-proxmox-daemon-0.1+default-dev <!nocheck>,
librust-proxmox-lang-1+default-dev (>= 1.3-~~) <!nocheck>,
librust-proxmox-log-0.2+default-dev (>= 0.2.3-~~) <!nocheck>,
librust-proxmox-log-0.2+default-dev (>= 0.2.5-~~) <!nocheck>,
librust-proxmox-router-3+default-dev <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.2-~~) <!nocheck>,
librust-proxmox-schema-3+default-dev (>= 3.1.2-~~) <!nocheck>,
@ -43,9 +43,10 @@ Build-Depends: debhelper (>= 12),
librust-tracing-0.1+default-dev <!nocheck>,
librust-url-2+default-dev (>= 2.2-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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.

View File

@ -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("&lt;"),
'>' => output.push_str("&gt;"),
'"' => output.push_str("&quot;"),
'&' => output.push_str("&amp;"),
'\'' => output.push_str("&#x27;"),
'`' => output.push_str("&#x60;"),
_ => output.push(c),
}
}
output
});
Self {
templates: RwLock::new(registry),
template_files: RwLock::new(HashMap::new()),
}
}
pub fn register<P>(&self, name: &str, path: P) -> Result<(), Error>
where
P: Into<PathBuf>,

View File

@ -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<SslAcceptor, Error> {
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)),

View File

@ -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.

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-env-logger-0.10+default-dev <!nocheck>,
@ -25,9 +25,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-plain-1+default-dev <!nocheck>,
librust-unicode-width-0.1+default-dev (>= 0.1.8-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-plain-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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

View File

@ -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 <support@proxmox.com> 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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-json-1+default-dev <!nocheck>,
librust-serde-plain-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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();

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-json-1+default-dev <!nocheck>,
librust-textwrap-0.16+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 = []

View File

@ -0,0 +1,5 @@
rust-proxmox-sendmail (0.1.0-1) bookworm; urgency=medium
* Initial release.
-- Proxmox Support Team <support@proxmox.com> Mon, 02 Dec 2024 14:47:42 +0100

View File

@ -0,0 +1,43 @@
Source: rust-proxmox-sendmail
Section: rust
Priority: optional
Build-Depends: debhelper-compat (= 13),
dh-sequence-cargo,
cargo:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-base64-0.13+default-dev <!nocheck>,
librust-percent-encoding-2+default-dev (>= 2.1-~~) <!nocheck>,
librust-proxmox-time-2+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -0,0 +1,18 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files:
*
Copyright: 2019 - 2024 Proxmox Server Solutions GmbH <support@proxmox.com>
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 <https://www.gnu.org/licenses/>.

View File

@ -0,0 +1,7 @@
overlay = "."
crate_src_path = ".."
maintainer = "Proxmox Support Team <support@proxmox.com>"
[source]
vcs_git = "git://git.proxmox.com/git/proxmox.git"
vcs_browser = "https://git.proxmox.com/?p=proxmox.git"

836
proxmox-sendmail/src/lib.rs Normal file
View File

@ -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<String>,
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::<String>(),
);
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<Recipient>,
body_txt: String,
body_html: Option<String>,
attachments: Vec<Attachment<'a>>,
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::<Vec<&String>>())
.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<u32>,
) -> 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<String, Error> {
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::<String>(),
);
write!(mail, "\n--{file_boundary}--")?;
}
Ok(mail)
}
fn format_header(
&self,
now: i64,
file_boundary: &str,
html_boundary: &str,
) -> Result<String, Error> {
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::<Vec<String>>()
.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<String, Error> {
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 <mailfrom@example.com>
To: Receiver Name <receiver@example.com>
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 <mailfrom@example.com>
To: Undisclosed <noreply>
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 <mailfrom@example.com>
To: Receiver Name <receiver@example.com>, two@example.com, =?utf-8?B?bcOkeCBtw7xzdGVybcOkbm4=?= <mm@example.com>
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==?= <differentfrom@example.com>
To: =?utf-8?B?UmVjZWl2ZXIgTmFtZfCfk6k=?= <receiver@example.com>
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("<html lang=\"de-at\"><head></head><body>\n\t<pre>\n\t\tLorem Ipsum Dolor Sit Amet\n\t</pre>\n</body></html>");
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 <from@example.com>
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
<html lang="de-at"><head></head><body>
<pre>
Lorem Ipsum Dolor Sit Amet
</pre>
</body></html>
------_=_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 <from@example.com>
To: Receiver Name <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_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("<html lang=\"de-at\"><head></head><body>\n\t<pre>\n\t\tLorem Ipsum Dolor Sit Amet\n\t</pre>\n</body></html>");
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 <from@example.com>
To: Receiver Name <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_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
<html lang="de-at"><head></head><body>
<pre>
Lorem Ipsum Dolor Sit Amet
</pre>
</body></html>
------_=_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("<body>This is the HTML body</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 <foobar@example.com>
To: Tony Est <test@example.com>
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
<body>This is the HTML body</body>
------_=_NextPart_002_1718977850--"#
);
}
}

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
uuid-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -15,9 +15,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-json-1+default-dev <!nocheck>,
librust-serde-json-1+raw-value-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -11,9 +11,10 @@ Build-Depends: debhelper (>= 12),
librust-nix-0.26+default-dev (>= 0.26.1-~~) <!nocheck>,
librust-proxmox-sys-0.6+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -14,9 +14,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12),
librust-syn-2+full-dev <!nocheck>,
librust-syn-2+visit-mut-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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"

View File

@ -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"]

View File

@ -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 <support@proxmox.com> 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

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -10,9 +10,9 @@ Build-Depends: debhelper (>= 12),
librust-base64-0.13+default-dev <!nocheck>,
librust-hex-0.4+default-dev <!nocheck>,
librust-openssl-0.10+default-dev <!nocheck>,
librust-proxmox-http-0.9+client-trait-dev (>= 0.9.2-~~) <!nocheck>,
librust-proxmox-http-0.9+default-dev (>= 0.9.2-~~) <!nocheck>,
librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.2-~~) <!nocheck>,
librust-proxmox-http-0.9+client-trait-dev (>= 0.9.4-~~) <!nocheck>,
librust-proxmox-http-0.9+default-dev (>= 0.9.4-~~) <!nocheck>,
librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.4-~~) <!nocheck>,
librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-sys-0.6+default-dev <!nocheck>,
@ -21,9 +21,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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.

View File

@ -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;

View File

@ -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<String>,
}
impl SubscriptionInfo {
/// Returns the canonicalized signed data and, if available, signature contained in `self`.
pub fn signed_data(&self) -> Result<(Vec<u8>, Option<String>), 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<u8>, Option<String>), 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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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<ProductType, Error> {
self.key
.as_ref()
.ok_or_else(|| format_err!("no product key set"))
.map(|key| key[..3].parse::<ProductType>())?
}
pub fn get_next_due_date(&self) -> Result<i64, Error> {
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<ProductType, Error> {
self.key
.as_ref()
.ok_or_else(|| format_err!("no product key set"))
.map(|key| key[..3].parse::<ProductType>())?
/// Shortcut for md5 sums.
pub(crate) fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> {
hash(MessageDigest::md5(), data).map_err(Error::from)
}
pub fn get_next_due_date(&self) -> Result<i64, Error> {
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<String, Error> {
static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub";
/// Shortcut for md5 sums.
pub(crate) fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> {
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<String, Error> {
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<i64, Error> {
let mut components = value.split('-');
let year = components
.next()
.ok_or_else(|| format_err!("missing year component."))?
.parse::<i32>()?;
let month = components
.next()
.ok_or_else(|| format_err!("missing month component."))?
.parse::<i32>()?;
let day = components
.next()
.ok_or_else(|| format_err!("missing day component."))?
.parse::<i32>()?;
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<i64, Error> {
let mut components = value.split('-');
let year = components
.next()
.ok_or_else(|| format_err!("missing year component."))?
.parse::<i32>()?;
let month = components
.next()
.ok_or_else(|| format_err!("missing month component."))?
.parse::<i32>()?;
let day = components
.next()
.ok_or_else(|| format_err!("missing day component."))?
.parse::<i32>()?;
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()
}
}

View File

@ -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 <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -19,9 +19,10 @@ Build-Depends: debhelper (>= 12),
libacl1-dev <!nocheck>,
uuid-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -24,7 +24,7 @@ pub fn change_cloexec(fd: RawFd, on: bool) -> Result<(), anyhow::Error> {
}
pub(crate) fn cwd() -> Result<OwnedFd, nix::Error> {
open(".", OFlag::O_DIRECTORY, stat::Mode::empty())
open(".", crate::fs::DIR_FLAGS, stat::Mode::empty())
}
pub fn open<P>(path: &P, oflag: OFlag, mode: Mode) -> Result<OwnedFd, nix::Error>

View File

@ -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<P: AsRef<Path>>(
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<P: AsRef<Path>>(
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>(())
}) {

View File

@ -116,6 +116,29 @@ pub fn file_read_firstline<P: AsRef<Path>>(path: P) -> Result<String, Error> {
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<P: ?Sized + NixPath>(
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<P: AsRef<Path>>(
@ -127,7 +150,7 @@ pub fn make_tmp_file<P: AsRef<Path>>(
// 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),
};

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
rustc:native (>= 1.80) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) <!nocheck>,
@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-libc-0.2+default-dev (>= 0.2.107-~~) <!nocheck>,
libsystemd-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-plain-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
@ -13,9 +13,10 @@ Build-Depends: debhelper (>= 12),
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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

View File

@ -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 <support@proxmox.com> Tue, 26 Nov 2024 16:52:28 +0100
rust-proxmox-time (2.0.2-1) bookworm; urgency=medium
* time span:

View File

@ -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"

View File

@ -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<TimeSpan> 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<TimeSpan> for f64 {
}
}
impl From<TimeSpan> for f64 {
fn from(ts: TimeSpan) -> Self {
Self::from(&ts)
}
}
impl From<std::time::Duration> 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() {

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-js-sys-0.3+default-dev (>= 0.3.55-~~) <!nocheck>,
uuid-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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

View File

@ -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 <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
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