[pmg-devel] [PATCH perl-rs 4/7] import pmg-rs
Wolfgang Bumiller
w.bumiller at proxmox.com
Fri Nov 26 14:55:15 CET 2021
Signed-off-by: Wolfgang Bumiller <w.bumiller at proxmox.com>
---
Cargo.toml | 1 +
Makefile | 7 +-
pmg-rs/Cargo.toml | 33 +++
pmg-rs/Makefile | 75 ++++++
pmg-rs/debian/changelog | 50 ++++
pmg-rs/debian/compat | 1 +
pmg-rs/debian/control | 27 +++
pmg-rs/debian/copyright | 16 ++
pmg-rs/debian/debcargo.toml | 10 +
pmg-rs/debian/rules | 7 +
pmg-rs/debian/source/format | 1 +
pmg-rs/debian/triggers | 1 +
pmg-rs/src/acme.rs | 430 +++++++++++++++++++++++++++++++++
pmg-rs/src/apt/mod.rs | 1 +
pmg-rs/src/apt/repositories.rs | 162 +++++++++++++
pmg-rs/src/csr.rs | 24 ++
pmg-rs/src/lib.rs | 3 +
pmg-rs/test.pl | 172 +++++++++++++
18 files changed, 1018 insertions(+), 3 deletions(-)
create mode 100644 pmg-rs/Cargo.toml
create mode 100644 pmg-rs/Makefile
create mode 100644 pmg-rs/debian/changelog
create mode 100644 pmg-rs/debian/compat
create mode 100644 pmg-rs/debian/control
create mode 100644 pmg-rs/debian/copyright
create mode 100644 pmg-rs/debian/debcargo.toml
create mode 100755 pmg-rs/debian/rules
create mode 100644 pmg-rs/debian/source/format
create mode 100644 pmg-rs/debian/triggers
create mode 100644 pmg-rs/src/acme.rs
create mode 100644 pmg-rs/src/apt/mod.rs
create mode 100644 pmg-rs/src/apt/repositories.rs
create mode 100644 pmg-rs/src/csr.rs
create mode 100644 pmg-rs/src/lib.rs
create mode 100644 pmg-rs/test.pl
diff --git a/Cargo.toml b/Cargo.toml
index 8556b45..6b869d4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
exclude = [ "build", "perl-*" ]
members = [
"pve-rs",
+ "pmg-rs",
]
[patch.crates-io]
diff --git a/Makefile b/Makefile
index f8dd85a..2cc02fe 100644
--- a/Makefile
+++ b/Makefile
@@ -28,14 +28,15 @@ build:
echo system >build/rust-toolchain
cp -a ./perl-* ./build/
cp -a ./pve-rs ./build
+ cp -a ./pmg-rs ./build
pve-deb: build
cd ./build/pve-rs && dpkg-buildpackage -b -uc -us
touch $@
-# pmg-deb: build
-# cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
-# touch $@
+pmg-deb: build
+ cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
+ touch $@
%-upload: %-deb
cd build; \
diff --git a/pmg-rs/Cargo.toml b/pmg-rs/Cargo.toml
new file mode 100644
index 0000000..02f59de
--- /dev/null
+++ b/pmg-rs/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "pmg-rs"
+version = "0.3.2"
+authors = [
+ "Proxmox Support Team <support at proxmox.com>",
+ "Wolfgang Bumiller <w.bumiller at proxmox.com>",
+ "Fabian Ebner <f.ebner at proxmox.com>",
+]
+edition = "2018"
+license = "AGPL-3"
+description = "PMG parts which have been ported to rust"
+exclude = [
+ "build",
+ "debian",
+ "PMG",
+]
+
+[lib]
+crate-type = [ "cdylib" ]
+
+[dependencies]
+anyhow = "1.0"
+hex = "0.4"
+openssl = "0.10.32"
+serde = "1.0"
+serde_bytes = "0.11.3"
+serde_json = "1.0"
+
+perlmod = { version = "0.8.1", features = [ "exporter" ] }
+
+proxmox-acme-rs = { version = "0.3.1", features = ["client"] }
+
+proxmox-apt = "0.8.0"
diff --git a/pmg-rs/Makefile b/pmg-rs/Makefile
new file mode 100644
index 0000000..a290544
--- /dev/null
+++ b/pmg-rs/Makefile
@@ -0,0 +1,75 @@
+include /usr/share/dpkg/default.mk
+
+PACKAGE=libpmg-rs-perl
+
+ARCH:=$(shell dpkg-architecture -qDEB_BUILD_ARCH)
+export GITVERSION:=$(shell git rev-parse HEAD)
+
+PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
+PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
+
+MAIN_DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb
+DBGSYM_DEB=${PACKAGE}-dbgsym_${DEB_VERSION}_${ARCH}.deb
+DEBS=$(MAIN_DEB) $(DBGSYM_DEB)
+
+DESTDIR=
+
+PM_DIRS := \
+ PMG/RS/APT
+
+PM_FILES := \
+ PMG/RS/Acme.pm \
+ PMG/RS/APT/Repositories.pm \
+ PMG/RS/CSR.pm
+
+ifeq ($(BUILD_MODE), release)
+CARGO_BUILD_ARGS += --release
+endif
+
+all:
+ifneq ($(BUILD_MODE), skip)
+ cargo build $(CARGO_BUILD_ARGS)
+endif
+
+# always re-create this dir
+# but also copy the local target/ and PMG/ dirs as a build-cache
+.PHONY: build
+build:
+ rm -rf build
+ cargo build --release
+ rsync -a debian Makefile Cargo.toml Cargo.lock src target PMG build/
+
+.PHONY: install
+install: target/release/libpmg_rs.so
+ install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto
+ install -m644 target/release/libpmg_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpmg_rs.so
+ install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)/PMG/RS
+ for i in $(PM_DIRS); do \
+ install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)/$$i; \
+ done
+ for i in $(PM_FILES); do \
+ install -m644 $$i $(DESTDIR)$(PERL_INSTALLVENDORLIB)/$$i; \
+ done
+
+.PHONY: deb
+deb: $(MAIN_DEB)
+$(MAIN_DEB): build
+ cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
+ lintian $(DEBS)
+
+distclean: clean
+
+clean:
+ cargo clean
+ rm -rf *.deb *.dsc *.tar.gz *.buildinfo *.changes Cargo.lock build
+ find . -name '*~' -exec rm {} ';'
+
+.PHONY: dinstall
+dinstall: ${DEBS}
+ dpkg -i ${DEBS}
+
+.PHONY: upload
+upload: ${DEBS}
+ # check if working directory is clean
+ git diff --exit-code --stat && git diff --exit-code --stat --staged
+ tar cf - ${DEBS} | ssh -X repoman at repo.proxmox.com upload --product pmg --dist bullseye
diff --git a/pmg-rs/debian/changelog b/pmg-rs/debian/changelog
new file mode 100644
index 0000000..afc3e60
--- /dev/null
+++ b/pmg-rs/debian/changelog
@@ -0,0 +1,50 @@
+libpmg-rs-perl (0.3.2) bullseye; urgency=medium
+
+ * acme: add proxy support
+
+ -- Proxmox Support Team <support at proxmox.com> Thu, 18 Nov 2021 11:18:01 +0100
+
+libpmg-rs-perl (0.3.1) bullseye; urgency=medium
+
+ * update to proxmox-acme-rs 0.3
+
+ -- Proxmox Support Team <support at proxmox.com> Thu, 21 Oct 2021 13:13:46 +0200
+
+libpmg-rs-perl (0.3.0) bullseye; urgency=medium
+
+ * update proxmox-apt to 0.6.0
+
+ -- Proxmox Support Team <support at proxmox.com> Fri, 30 Jul 2021 10:56:35 +0200
+
+libpmg-rs-perl (0.2.0-1) bullseye; urgency=medium
+
+ * add bindings for proxmox-apt
+
+ -- Proxmox Support Team <support at proxmox.com> Tue, 13 Jul 2021 12:48:04 +0200
+
+libpmg-rs-perl (0.1.3-1) bullseye; urgency=medium
+
+ * re-build for Proxmox Mail Gateway 7 / Debian 11 Bullseye
+
+ -- Proxmox Support Team <support at proxmox.com> Thu, 27 May 2021 19:58:08 +0200
+
+libpmg-rs-perl (0.1.2-1) buster; urgency=medium
+
+ * update proxmox-acme-rs to 0.1.4 to store the 'created' account field if it
+ is available
+
+ * set account file permission to 0700
+
+ -- Proxmox Support Team <support at proxmox.com> Mon, 29 Mar 2021 11:22:54 +0200
+
+libpmg-rs-perl (0.1.1-1) unstable; urgency=medium
+
+ * update proxmox-acme-rs to 0.1.3 to fix ecsda signature padding
+
+ -- Proxmox Support Team <support at proxmox.com> Wed, 17 Mar 2021 13:43:12 +0100
+
+libpmg-rs-perl (0.1-1) unstable; urgency=medium
+
+ * initial release
+
+ -- Proxmox Support Team <support at proxmox.com> Mon, 22 Feb 2021 13:40:10 +0100
diff --git a/pmg-rs/debian/compat b/pmg-rs/debian/compat
new file mode 100644
index 0000000..48082f7
--- /dev/null
+++ b/pmg-rs/debian/compat
@@ -0,0 +1 @@
+12
diff --git a/pmg-rs/debian/control b/pmg-rs/debian/control
new file mode 100644
index 0000000..be632ba
--- /dev/null
+++ b/pmg-rs/debian/control
@@ -0,0 +1,27 @@
+Source: libpmg-rs-perl
+Section: perl
+Priority: optional
+Maintainer: Proxmox Support Team <support at proxmox.com>
+Build-Depends:
+ debhelper (>= 12),
+ librust-anyhow-1+default-dev,
+ librust-hex-0.4+default-dev,
+ librust-openssl-0.10+default-dev (>= 0.10.32-~~),
+ librust-perlmod-0.8+default-dev,
+ librust-perlmod-0.8+exporter-dev,
+ librust-proxmox-acme-rs-0.3+client-dev (>= 0.3.1-~~),
+ librust-proxmox-acme-rs-0.3+default-dev (>= 0.3.1-~~),
+ librust-proxmox-apt-0.8+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-bytes-0.11+default-dev (>= 0.11.3-~~),
+ librust-serde-json-1+default-dev,
+Standards-Version: 4.3.0
+Homepage: https://www.proxmox.com
+
+Package: libpmg-rs-perl
+Architecture: any
+Depends: ${perl:Depends},
+ ${shlibs:Depends},
+Description: Components of Proxmox Mail Gateway which have been ported to Rust.
+ Contains parts of Proxmox Mail Gateway which have been ported to, or newly
+ implemented in the Rust programming language.
diff --git a/pmg-rs/debian/copyright b/pmg-rs/debian/copyright
new file mode 100644
index 0000000..477c305
--- /dev/null
+++ b/pmg-rs/debian/copyright
@@ -0,0 +1,16 @@
+Copyright (C) 2020-2021 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support at proxmox.com>
+
+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 <http://www.gnu.org/licenses/>.
diff --git a/pmg-rs/debian/debcargo.toml b/pmg-rs/debian/debcargo.toml
new file mode 100644
index 0000000..8aa085f
--- /dev/null
+++ b/pmg-rs/debian/debcargo.toml
@@ -0,0 +1,10 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support at proxmox.com>"
+
+[source]
+section = "perl"
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
+
+[packages.libpmg-rs-perl]
diff --git a/pmg-rs/debian/rules b/pmg-rs/debian/rules
new file mode 100755
index 0000000..0f5be05
--- /dev/null
+++ b/pmg-rs/debian/rules
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+
+#export DH_VERBOSE=1
+export BUILD_MODE=release
+
+%:
+ dh $@
diff --git a/pmg-rs/debian/source/format b/pmg-rs/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/pmg-rs/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/pmg-rs/debian/triggers b/pmg-rs/debian/triggers
new file mode 100644
index 0000000..59dd688
--- /dev/null
+++ b/pmg-rs/debian/triggers
@@ -0,0 +1 @@
+activate-noawait pve-api-updates
diff --git a/pmg-rs/src/acme.rs b/pmg-rs/src/acme.rs
new file mode 100644
index 0000000..0429a0d
--- /dev/null
+++ b/pmg-rs/src/acme.rs
@@ -0,0 +1,430 @@
+//! `PMG::RS::Acme` perl module.
+//!
+//! The functions in here are perl bindings.
+
+use std::fs::OpenOptions;
+use std::io::{self, Write};
+use std::os::unix::fs::OpenOptionsExt;
+
+use anyhow::{format_err, Error};
+use serde::{Deserialize, Serialize};
+
+use proxmox_acme_rs::account::AccountData as AcmeAccountData;
+use proxmox_acme_rs::{Account, Client};
+
+/// Our on-disk format inherited from PVE's proxmox-acme code.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AccountData {
+ /// The account's location URL.
+ location: String,
+
+ /// The account dat.
+ account: AcmeAccountData,
+
+ /// The private key as PEM formatted string.
+ key: String,
+
+ /// ToS URL the user agreed to.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tos: Option<String>,
+
+ #[serde(skip_serializing_if = "is_false", default)]
+ debug: bool,
+
+ /// The directory's URL.
+ directory_url: String,
+}
+
+#[inline]
+fn is_false(b: &bool) -> bool {
+ !*b
+}
+
+struct Inner {
+ client: Client,
+ account_path: Option<String>,
+ tos: Option<String>,
+ debug: bool,
+}
+
+impl Inner {
+ pub fn new(api_directory: String) -> Result<Self, Error> {
+ Ok(Self {
+ client: Client::new(api_directory),
+ account_path: None,
+ tos: None,
+ debug: false,
+ })
+ }
+
+ pub fn load(account_path: String) -> Result<Self, Error> {
+ let data = std::fs::read(&account_path)?;
+ let data: AccountData = serde_json::from_slice(&data)?;
+
+ let mut client = Client::new(data.directory_url);
+ client.set_account(Account::from_parts(data.location, data.key, data.account));
+
+ Ok(Self {
+ client,
+ account_path: Some(account_path),
+ tos: data.tos,
+ debug: data.debug,
+ })
+ }
+
+ pub fn new_account(
+ &mut self,
+ account_path: String,
+ tos_agreed: bool,
+ contact: Vec<String>,
+ rsa_bits: Option<u32>,
+ ) -> Result<(), Error> {
+ self.tos = if tos_agreed {
+ self.client.terms_of_service_url()?.map(str::to_owned)
+ } else {
+ None
+ };
+
+ let _account = self.client.new_account(contact, tos_agreed, rsa_bits)?;
+ let file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .mode(0o600)
+ .open(&account_path)
+ .map_err(|err| format_err!("failed to open {:?} for writing: {}", account_path, err))?;
+ self.write_to(file).map_err(|err| {
+ format_err!(
+ "failed to write acme account to {:?}: {}",
+ account_path,
+ err
+ )
+ })?;
+ self.account_path = Some(account_path);
+
+ Ok(())
+ }
+
+ /// Convenience helper around `.client.account().ok_or_else(||...)`
+ fn account(&self) -> Result<&Account, Error> {
+ self.client
+ .account()
+ .ok_or_else(|| format_err!("missing account"))
+ }
+
+ fn to_account_data(&self) -> Result<AccountData, Error> {
+ let account = self.account()?;
+
+ Ok(AccountData {
+ location: account.location.clone(),
+ key: account.private_key.clone(),
+ account: AcmeAccountData {
+ only_return_existing: false, // don't actually write this out in case it's set
+ ..account.data.clone()
+ },
+ tos: self.tos.clone(),
+ debug: self.debug,
+ directory_url: self.client.directory_url().to_owned(),
+ })
+ }
+
+ fn write_to<T: io::Write>(&mut self, out: T) -> Result<(), Error> {
+ let data = self.to_account_data()?;
+
+ Ok(serde_json::to_writer_pretty(out, &data)?)
+ }
+
+ pub fn update_account<T: Serialize>(&mut self, data: &T) -> Result<(), Error> {
+ let account_path = self
+ .account_path
+ .as_deref()
+ .ok_or_else(|| format_err!("missing account path"))?;
+ self.client.update_account(data)?;
+
+ let tmp_path = format!("{}.tmp", account_path);
+ // FIXME: move proxmox::tools::replace_file & make_temp out into a nice *little* crate...
+ let mut file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .mode(0o600)
+ .open(&tmp_path)
+ .map_err(|err| format_err!("failed to open {:?} for writing: {}", tmp_path, err))?;
+ self.write_to(&mut file).map_err(|err| {
+ format_err!("failed to write acme account to {:?}: {}", tmp_path, err)
+ })?;
+ file.flush().map_err(|err| {
+ format_err!("failed to flush acme account file {:?}: {}", tmp_path, err)
+ })?;
+
+ // re-borrow since we needed `self` as mut earlier
+ let account_path = self.account_path.as_deref().unwrap();
+ std::fs::rename(&tmp_path, account_path).map_err(|err| {
+ format_err!(
+ "failed to rotate temp file into place ({:?} -> {:?}): {}",
+ &tmp_path,
+ account_path,
+ err
+ )
+ })?;
+ drop(file);
+ Ok(())
+ }
+
+ pub fn revoke_certificate(&mut self, data: &[u8], reason: Option<u32>) -> Result<(), Error> {
+ Ok(self.client.revoke_certificate(data, reason)?)
+ }
+
+ pub fn set_proxy(&mut self, proxy: String) {
+ self.client.set_proxy(proxy)
+ }
+}
+
+#[perlmod::package(name = "PMG::RS::Acme", lib = "pmg_rs")]
+pub mod export {
+ use std::collections::HashMap;
+ use std::convert::TryFrom;
+ use std::sync::Mutex;
+
+ use anyhow::Error;
+ use serde_bytes::{ByteBuf, Bytes};
+
+ use perlmod::Value;
+ use proxmox_acme_rs::directory::Meta;
+ use proxmox_acme_rs::order::OrderData;
+ use proxmox_acme_rs::{Authorization, Challenge, Order};
+
+ use super::{AccountData, Inner};
+
+ const CLASSNAME: &str = "PMG::RS::Acme";
+
+ /// An Acme client instance.
+ pub struct Acme {
+ inner: Mutex<Inner>,
+ }
+
+ impl<'a> TryFrom<&'a Value> for &'a Acme {
+ type Error = Error;
+
+ fn try_from(value: &'a Value) -> Result<&'a Acme, Error> {
+ Ok(unsafe { value.from_blessed_box(CLASSNAME)? })
+ }
+ }
+
+ fn bless(class: Value, mut ptr: Box<Acme>) -> Result<Value, Error> {
+ let value = Value::new_pointer::<Acme>(&mut *ptr);
+ let value = Value::new_ref(&value);
+ let this = value.bless_sv(&class)?;
+ let _perl = Box::leak(ptr);
+ Ok(this)
+ }
+
+ /// Create a new ACME client instance given an account path and an API directory URL.
+ #[export(raw_return)]
+ pub fn new(#[raw] class: Value, api_directory: String) -> Result<Value, Error> {
+ bless(
+ class,
+ Box::new(Acme {
+ inner: Mutex::new(Inner::new(api_directory)?),
+ }),
+ )
+ }
+
+ /// Load an existing account.
+ #[export(raw_return)]
+ pub fn load(#[raw] class: Value, account_path: String) -> Result<Value, Error> {
+ bless(
+ class,
+ Box::new(Acme {
+ inner: Mutex::new(Inner::load(account_path)?),
+ }),
+ )
+ }
+
+ #[export(name = "DESTROY")]
+ fn destroy(#[raw] this: Value) {
+ perlmod::destructor!(this, Acme: CLASSNAME);
+ }
+
+ /// Create a new account.
+ ///
+ /// `tos_agreed` is usually not optional, but may be set later via an update.
+ /// The `contact` list should be a list of `mailto:` strings (or others, if the directory
+ /// allows the).
+ ///
+ /// In case an RSA key should be generated, an `rsa_bits` parameter should be provided.
+ /// Otherwise a P-256 EC key will be generated.
+ #[export]
+ pub fn new_account(
+ #[try_from_ref] this: &Acme,
+ account_path: String,
+ tos_agreed: bool,
+ contact: Vec<String>,
+ rsa_bits: Option<u32>,
+ ) -> Result<(), Error> {
+ this.inner
+ .lock()
+ .unwrap()
+ .new_account(account_path, tos_agreed, contact, rsa_bits)
+ }
+
+ /// Get the directory's meta information.
+ #[export]
+ pub fn get_meta(#[try_from_ref] this: &Acme) -> Result<Option<Meta>, Error> {
+ match this.inner.lock().unwrap().client.directory()?.meta() {
+ Some(meta) => Ok(Some(meta.clone())),
+ None => Ok(None),
+ }
+ }
+
+ /// Get the account's directory URL.
+ #[export]
+ pub fn directory(#[try_from_ref] this: &Acme) -> Result<String, Error> {
+ Ok(this.inner.lock().unwrap().client.directory()?.url.clone())
+ }
+
+ /// Serialize the account data.
+ #[export]
+ pub fn account(#[try_from_ref] this: &Acme) -> Result<AccountData, Error> {
+ this.inner.lock().unwrap().to_account_data()
+ }
+
+ /// Get the account's location URL.
+ #[export]
+ pub fn location(#[try_from_ref] this: &Acme) -> Result<String, Error> {
+ Ok(this.inner.lock().unwrap().account()?.location.clone())
+ }
+
+ /// Get the account's agreed-to ToS URL.
+ #[export]
+ pub fn tos_url(#[try_from_ref] this: &Acme) -> Option<String> {
+ this.inner.lock().unwrap().tos.clone()
+ }
+
+ /// Get the debug flag.
+ #[export]
+ pub fn debug(#[try_from_ref] this: &Acme) -> bool {
+ this.inner.lock().unwrap().debug
+ }
+
+ /// Get the debug flag.
+ #[export]
+ pub fn set_debug(#[try_from_ref] this: &Acme, on: bool) {
+ this.inner.lock().unwrap().debug = on;
+ }
+
+ /// Place a new order.
+ #[export]
+ pub fn new_order(
+ #[try_from_ref] this: &Acme,
+ domains: Vec<String>,
+ ) -> Result<(String, OrderData), Error> {
+ let order: Order = this.inner.lock().unwrap().client.new_order(domains)?;
+ Ok((order.location, order.data))
+ }
+
+ /// Get the authorization info given an authorization URL.
+ ///
+ /// This should be an URL found in the `authorizations` array in the `OrderData` returned from
+ /// `new_order`.
+ #[export]
+ pub fn get_authorization(
+ #[try_from_ref] this: &Acme,
+ url: &str,
+ ) -> Result<Authorization, Error> {
+ Ok(this.inner.lock().unwrap().client.get_authorization(url)?)
+ }
+
+ /// Query an order given its URL.
+ ///
+ /// The corresponding URL is returned as first value from the `new_order` call.
+ #[export]
+ pub fn get_order(#[try_from_ref] this: &Acme, url: &str) -> Result<OrderData, Error> {
+ Ok(this.inner.lock().unwrap().client.get_order(url)?)
+ }
+
+ /// Get the key authorization string for a challenge given a token.
+ #[export]
+ pub fn key_authorization(#[try_from_ref] this: &Acme, token: &str) -> Result<String, Error> {
+ Ok(this.inner.lock().unwrap().client.key_authorization(token)?)
+ }
+
+ /// Get the key dns-01 TXT challenge value for a token.
+ #[export]
+ pub fn dns_01_txt_value(#[try_from_ref] this: &Acme, token: &str) -> Result<String, Error> {
+ Ok(this.inner.lock().unwrap().client.dns_01_txt_value(token)?)
+ }
+
+ /// Request validation of a challenge by URL.
+ ///
+ /// Given an `Authorization`, it'll contain `challenges`. These contain `url`s pointing to a
+ /// method used to request challenge authorization. This is the URL used for this method,
+ /// *after* performing the necessary steps to satisfy the challenge. (Eg. after setting up a
+ /// DNS TXT entry using the `dns-01` type challenge's key authorization.
+ #[export]
+ pub fn request_challenge_validation(
+ #[try_from_ref] this: &Acme,
+ url: &str,
+ ) -> Result<Challenge, Error> {
+ Ok(this
+ .inner
+ .lock()
+ .unwrap()
+ .client
+ .request_challenge_validation(url)?)
+ }
+
+ /// Request finalization of an order.
+ ///
+ /// The `url` should be the 'finalize' URL of the order.
+ #[export]
+ pub fn finalize_order(
+ #[try_from_ref] this: &Acme,
+ url: &str,
+ csr: &Bytes,
+ ) -> Result<(), Error> {
+ Ok(this.inner.lock().unwrap().client.finalize(url, csr)?)
+ }
+
+ /// Download the certificate for an order.
+ ///
+ /// The `url` should be the 'certificate' URL of the order.
+ #[export]
+ pub fn get_certificate(#[try_from_ref] this: &Acme, url: &str) -> Result<ByteBuf, Error> {
+ Ok(ByteBuf::from(
+ this.inner.lock().unwrap().client.get_certificate(url)?,
+ ))
+ }
+
+ /// Update account data.
+ ///
+ /// This can be used for example to deactivate an account or agree to ToS later on.
+ #[export]
+ pub fn update_account(
+ #[try_from_ref] this: &Acme,
+ data: HashMap<String, serde_json::Value>,
+ ) -> Result<(), Error> {
+ this.inner.lock().unwrap().update_account(&data)?;
+ Ok(())
+ }
+
+ /// Revoke an existing certificate using the certificate in PEM or DER form.
+ #[export]
+ pub fn revoke_certificate(
+ #[try_from_ref] this: &Acme,
+ data: &[u8],
+ reason: Option<u32>,
+ ) -> Result<(), Error> {
+ this.inner
+ .lock()
+ .unwrap()
+ .revoke_certificate(&data, reason)?;
+ Ok(())
+ }
+
+ /// Set a proxy
+ #[export]
+ pub fn set_proxy(#[try_from_ref] this: &Acme, proxy: String) {
+ this.inner.lock().unwrap().set_proxy(proxy)
+ }
+
+}
diff --git a/pmg-rs/src/apt/mod.rs b/pmg-rs/src/apt/mod.rs
new file mode 100644
index 0000000..574c1a7
--- /dev/null
+++ b/pmg-rs/src/apt/mod.rs
@@ -0,0 +1 @@
+mod repositories;
diff --git a/pmg-rs/src/apt/repositories.rs b/pmg-rs/src/apt/repositories.rs
new file mode 100644
index 0000000..75207c7
--- /dev/null
+++ b/pmg-rs/src/apt/repositories.rs
@@ -0,0 +1,162 @@
+#[perlmod::package(name = "PMG::RS::APT::Repositories", lib = "pmg_rs")]
+mod export {
+ use std::convert::TryInto;
+
+ use anyhow::{bail, Error};
+ use serde::{Deserialize, Serialize};
+
+ use proxmox_apt::repositories::{
+ APTRepositoryFile, APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo,
+ APTStandardRepository,
+ };
+
+ #[derive(Deserialize, Serialize)]
+ #[serde(rename_all = "kebab-case")]
+ /// Result for the repositories() function
+ pub struct RepositoriesResult {
+ /// Successfully parsed files.
+ pub files: Vec<APTRepositoryFile>,
+
+ /// Errors for files that could not be parsed or read.
+ pub errors: Vec<APTRepositoryFileError>,
+
+ /// Common digest for successfully parsed files.
+ pub digest: String,
+
+ /// Additional information/warnings about repositories.
+ pub infos: Vec<APTRepositoryInfo>,
+
+ /// Standard repositories and their configuration status.
+ pub standard_repos: Vec<APTStandardRepository>,
+ }
+
+ #[derive(Deserialize, Serialize)]
+ #[serde(rename_all = "kebab-case")]
+ /// For changing an existing repository.
+ pub struct ChangeProperties {
+ /// Whether the repository should be enabled or not.
+ pub enabled: Option<bool>,
+ }
+
+ /// Get information about configured and standard repositories.
+ #[export]
+ pub fn repositories() -> Result<RepositoriesResult, Error> {
+ let (files, errors, digest) = proxmox_apt::repositories::repositories()?;
+ let digest = hex::encode(&digest);
+
+ let suite = proxmox_apt::repositories::get_current_release_codename()?;
+
+ let infos = proxmox_apt::repositories::check_repositories(&files, suite);
+ let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pmg", suite);
+
+ Ok(RepositoriesResult {
+ files,
+ errors,
+ digest,
+ infos,
+ standard_repos,
+ })
+ }
+
+ /// Add the repository identified by the `handle`.
+ /// If the repository is already configured, it will be set to enabled.
+ ///
+ /// The `digest` parameter asserts that the configuration has not been modified.
+ #[export]
+ pub fn add_repository(handle: &str, digest: Option<&str>) -> Result<(), Error> {
+ let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
+
+ let handle: APTRepositoryHandle = handle.try_into()?;
+ let suite = proxmox_apt::repositories::get_current_release_codename()?;
+
+ if let Some(digest) = digest {
+ let expected_digest = hex::decode(digest)?;
+ if expected_digest != current_digest {
+ bail!("detected modified configuration - file changed by other user? Try again.");
+ }
+ }
+
+ // check if it's already configured first
+ for file in files.iter_mut() {
+ for repo in file.repositories.iter_mut() {
+ if repo.is_referenced_repository(handle, "pmg", &suite.to_string()) {
+ if repo.enabled {
+ return Ok(());
+ }
+
+ repo.set_enabled(true);
+ file.write()?;
+
+ return Ok(());
+ }
+ }
+ }
+
+ let (repo, path) = proxmox_apt::repositories::get_standard_repository(handle, "pmg", suite);
+
+ if let Some(error) = errors.iter().find(|error| error.path == path) {
+ bail!(
+ "unable to parse existing file {} - {}",
+ error.path,
+ error.error,
+ );
+ }
+
+ if let Some(file) = files.iter_mut().find(|file| file.path == path) {
+ file.repositories.push(repo);
+
+ file.write()?;
+ } else {
+ let mut file = match APTRepositoryFile::new(&path)? {
+ Some(file) => file,
+ None => bail!("invalid path - {}", path),
+ };
+
+ file.repositories.push(repo);
+
+ file.write()?;
+ }
+
+ Ok(())
+ }
+
+ /// Change the properties of the specified repository.
+ ///
+ /// The `digest` parameter asserts that the configuration has not been modified.
+ #[export]
+ pub fn change_repository(
+ path: &str,
+ index: usize,
+ options: ChangeProperties,
+ digest: Option<&str>,
+ ) -> Result<(), Error> {
+ let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
+
+ if let Some(digest) = digest {
+ let expected_digest = hex::decode(digest)?;
+ if expected_digest != current_digest {
+ bail!("detected modified configuration - file changed by other user? Try again.");
+ }
+ }
+
+ if let Some(error) = errors.iter().find(|error| error.path == path) {
+ bail!("unable to parse file {} - {}", error.path, error.error);
+ }
+
+ if let Some(file) = files.iter_mut().find(|file| file.path == path) {
+ if let Some(repo) = file.repositories.get_mut(index) {
+ if let Some(enabled) = options.enabled {
+ repo.set_enabled(enabled);
+ }
+
+ file.write()?;
+ } else {
+ bail!("invalid index - {}", index);
+ }
+ } else {
+ bail!("invalid path - {}", path);
+ }
+
+ Ok(())
+ }
+}
diff --git a/pmg-rs/src/csr.rs b/pmg-rs/src/csr.rs
new file mode 100644
index 0000000..961a2cf
--- /dev/null
+++ b/pmg-rs/src/csr.rs
@@ -0,0 +1,24 @@
+#[perlmod::package(name = "PMG::RS::CSR", lib = "pmg_rs")]
+pub mod export {
+ use std::collections::HashMap;
+
+ use anyhow::Error;
+ use serde_bytes::ByteBuf;
+
+ use proxmox_acme_rs::util::Csr;
+
+ /// Generates a CSR and its accompanying private key.
+ ///
+ /// The CSR is DER formatted, the private key is a PEM formatted pkcs8 private key.
+ #[export]
+ pub fn generate_csr(
+ identifiers: Vec<&str>,
+ attributes: HashMap<String, &str>,
+ ) -> Result<(ByteBuf, ByteBuf), Error> {
+ let csr = Csr::generate(&identifiers, &attributes)?;
+ Ok((
+ ByteBuf::from(csr.data),
+ ByteBuf::from(csr.private_key_pem),
+ ))
+ }
+}
diff --git a/pmg-rs/src/lib.rs b/pmg-rs/src/lib.rs
new file mode 100644
index 0000000..47e61b5
--- /dev/null
+++ b/pmg-rs/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod acme;
+pub mod apt;
+pub mod csr;
diff --git a/pmg-rs/test.pl b/pmg-rs/test.pl
new file mode 100644
index 0000000..3d2a6df
--- /dev/null
+++ b/pmg-rs/test.pl
@@ -0,0 +1,172 @@
+#!/usr/bin/env perl
+
+use v5.28.0;
+use Data::Dumper;
+
+use lib '.';
+use PMG::RS::Acme;
+use PMG::RS::CSR;
+
+# "Config:" The Acme server URL:
+my $DIR = 'https://acme-staging-v02.api.letsencrypt.org/directory';
+
+# Useage:
+#
+# * Create a new account:
+# | ~/ $ ./test.pl ./account.json new 'somebody at example.invalid"
+#
+# The `./account.json` will be created using an EC P-256 key.
+# Optionally an RSA key size can be passed as additional parameter to generate
+# an account with an RSA key instead.
+#
+# From here on out the `./account.json` file must already exist:
+#
+# * Place a new order:
+# | ~/ $ ./test.pl ./account.json new-order my.domain.com
+# | $VAR1 = {
+# | ... order data ...
+# | 'authorizations' => [
+# | 'https://acme.example/auths/1244',
+# | ... possibly more ...
+# | ]
+# | }
+# | Order URL: https://acme.example/order/1793
+#
+# Note: This ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
+# URL will be used later for finalization and certifiate download.
+# The `$VAR1` dump contains the order JSON data.
+# The 'authorizations' URLs are going to be used next.
+#
+# * Get authorization info
+# | ~/ $ ./test.pl ./account.json get-auth 'https://acme.example/auths/1244'
+# | $VAR1 = {
+# | ... auth data ...
+# | 'challenges' => [
+# | {
+# | 'type' => 'dns-01',
+# | 'url' => 'https://acme.example/challenge/8188/dns1'
+# | }
+# | ... likely more ...
+# | ]
+# | }
+# | Key Authorization = SuperVeryMegaLongValue
+# | dns-01 TXT value = ShorterValue
+#
+# Now perform the things you need to for the challenge, eg. setup the DNS
+# entry using the provided TXT value.
+# Then use the correct challenge's URL with req-auth
+#
+# * Request challenge validation
+# | ~/ $ ./test.pl ./account.json \
+# | req-challenge 'https://acme.example/challenge/8188/dns1
+#
+# * Repeat the above 2 steps for all authorizations.
+# * Wait for the order to be valid via `get-order`
+# | ~/ $ ./test.pl ./account.json get-order 'https://acme.example/order/1793'
+# | $VAR1 = {
+# | 'status' => 'valid',
+# | 'finalize' => 'some URL',
+# | ... order data ...
+# | }
+# | Order URL: https://acme.example/order/1793
+#
+# * Finalize the order via the *Order URL* and a private key to sign the
+# request with (eg. generated via `openssl genrsa` or `openssl ecparam`).
+# | ~/ $ ./test.pl ./account.json \
+# | finalize my.domain.com ./my-private-key.pem \
+# | 'https://acme.example/order/1793'
+#
+# * Wait for a 'certificate' property to pop up in the order
+# (check via 'get-order')
+#
+# * Grab the certificate with the Order URL and a destination file name:
+# | ~/ $ ./test.pl ./account.json get-cert \
+# | 'https://acme.example/order/1793' \
+# | ./my-cert.pem
+
+
+my $account = shift // die "missing account file\n";
+my $cmd = shift // die "missing account file\n";
+
+sub load : prototype($) {
+ my ($file) = @_;
+ open(my $fh, '<', $file) or die "open($file): $!\n";
+ my $data = do {
+ local $/ = undef;
+ <$fh>
+ };
+ close($fh);
+ return $data;
+}
+
+sub store : prototype($$) {
+ my ($file, $data) = @_;
+ open(my $fh, '>', $file) or die "open($file): $!\n";
+ syswrite($fh, $data) == length($data)
+ or die "failed to write data to $file: $!\n";
+ close($fh);
+}
+
+if ($cmd eq 'new') {
+ my $mail = shift // die "missing mail address\n";
+ my $rsa_bits = shift;
+ if (defined($rsa_bits)) {
+ $rsa_bits = int($rsa_bits);
+ }
+ my $acme = PMG::RS::Acme->new($DIR);
+ $acme->new_account($account, 1, ["mailto:$mail"], undef);
+} elsif ($cmd eq 'get-meta') {
+ #my $acme = PMG::RS::Acme->new($DIR);
+ my $acme = PMG::RS::Acme->new('https%3A%2F%2Facme-v02.api.letsencrypt.org%2Fdirectory');
+ my $data = $acme->get_meta();
+ say Dumper($data);
+} elsif ($cmd eq 'new-order') {
+ my $domain = shift // die "missing domain\n";
+ my $acme = PMG::RS::Acme->load($account);
+ my ($url, $order) = $acme->new_order([$domain]);
+ say Dumper($order);
+ say "Order URL: $url\n";
+} elsif ($cmd eq 'get-auth') {
+ my $url = shift // die "missing url\n";
+ my $acme = PMG::RS::Acme->load($account);
+ my $auth = $acme->get_authorization($url);
+ say Dumper($auth);
+ for my $challenge ($auth->{challenges}->@*) {
+ next if $challenge->{type} ne 'dns-01';
+ say "Key Authorization = ".$acme->key_authorization($challenge->{token});
+ say "dns-01 TXT value = ".$acme->dns_01_txt_value($challenge->{token});
+ }
+} elsif ($cmd eq 'req-challenge') {
+ my $url = shift // die "missing url\n";
+ my $acme = PMG::RS::Acme->load($account);
+ my $challenge = $acme->request_challenge_validation($url);
+ say Dumper($challenge);
+} elsif ($cmd eq 'finalize') {
+ my $domain = shift // die 'missing domain\n';
+ my $pkfile = shift // die "missing private key file\n";
+ my $order_url = shift // die "missing order URL\n";
+ my ($csr_der, $pkey_pem) = PMG::RS::CSR::generate_csr([$domain], {});
+ store($pkfile, $pkey_pem);
+ my $acme = PMG::RS::Acme->load($account);
+ my $order = $acme->get_order($order_url);
+ say Dumper($order);
+ die "order not ready\n" if $order->{status} ne 'ready';
+ $acme->finalize_order($order->{finalize}, $csr_der);
+} elsif ($cmd eq 'get-order') {
+ my $order_url = shift // die "missing order URL\n";
+ my $acme = PMG::RS::Acme->load($account);
+ my $order = $acme->get_order($order_url);
+ say Dumper($order);
+} elsif ($cmd eq 'get-cert') {
+ my $order_url = shift // die "missing order URL\n";
+ my $file_name = shift // die "missing destination file name\n";
+ my $acme = PMG::RS::Acme->load($account);
+ my $order = $acme->get_order($order_url);
+ my $cert_url = $order->{certificate};
+ die "certificate not ready\n" if !$cert_url;
+ say Dumper($order);
+ my $cert = $acme->get_certificate($cert_url);
+ store($file_name, $cert);
+} else {
+ die "unknown command '$cmd'\n";
+}
--
2.30.2
More information about the pmg-devel
mailing list