import pmg-rs
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
73a88784f1
commit
b6274a49a5
@ -2,6 +2,7 @@
|
||||
exclude = [ "build", "perl-*" ]
|
||||
members = [
|
||||
"pve-rs",
|
||||
"pmg-rs",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
|
7
Makefile
7
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; \
|
||||
|
33
pmg-rs/Cargo.toml
Normal file
33
pmg-rs/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "pmg-rs"
|
||||
version = "0.3.2"
|
||||
authors = [
|
||||
"Proxmox Support Team <support@proxmox.com>",
|
||||
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
|
||||
"Fabian Ebner <f.ebner@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"
|
75
pmg-rs/Makefile
Normal file
75
pmg-rs/Makefile
Normal file
@ -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@repo.proxmox.com upload --product pmg --dist bullseye
|
50
pmg-rs/debian/changelog
Normal file
50
pmg-rs/debian/changelog
Normal file
@ -0,0 +1,50 @@
|
||||
libpmg-rs-perl (0.3.2) bullseye; urgency=medium
|
||||
|
||||
* acme: add proxy support
|
||||
|
||||
-- Proxmox Support Team <support@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@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@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@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@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@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@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@proxmox.com> Mon, 22 Feb 2021 13:40:10 +0100
|
1
pmg-rs/debian/compat
Normal file
1
pmg-rs/debian/compat
Normal file
@ -0,0 +1 @@
|
||||
12
|
27
pmg-rs/debian/control
Normal file
27
pmg-rs/debian/control
Normal file
@ -0,0 +1,27 @@
|
||||
Source: libpmg-rs-perl
|
||||
Section: perl
|
||||
Priority: optional
|
||||
Maintainer: Proxmox Support Team <support@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.
|
16
pmg-rs/debian/copyright
Normal file
16
pmg-rs/debian/copyright
Normal file
@ -0,0 +1,16 @@
|
||||
Copyright (C) 2020-2021 Proxmox Server Solutions GmbH
|
||||
|
||||
This software is written by Proxmox Server Solutions GmbH <support@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/>.
|
10
pmg-rs/debian/debcargo.toml
Normal file
10
pmg-rs/debian/debcargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
overlay = "."
|
||||
crate_src_path = ".."
|
||||
maintainer = "Proxmox Support Team <support@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]
|
7
pmg-rs/debian/rules
Executable file
7
pmg-rs/debian/rules
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
#export DH_VERBOSE=1
|
||||
export BUILD_MODE=release
|
||||
|
||||
%:
|
||||
dh $@
|
1
pmg-rs/debian/source/format
Normal file
1
pmg-rs/debian/source/format
Normal file
@ -0,0 +1 @@
|
||||
3.0 (native)
|
1
pmg-rs/debian/triggers
Normal file
1
pmg-rs/debian/triggers
Normal file
@ -0,0 +1 @@
|
||||
activate-noawait pve-api-updates
|
430
pmg-rs/src/acme.rs
Normal file
430
pmg-rs/src/acme.rs
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
1
pmg-rs/src/apt/mod.rs
Normal file
1
pmg-rs/src/apt/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
mod repositories;
|
162
pmg-rs/src/apt/repositories.rs
Normal file
162
pmg-rs/src/apt/repositories.rs
Normal file
@ -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(())
|
||||
}
|
||||
}
|
24
pmg-rs/src/csr.rs
Normal file
24
pmg-rs/src/csr.rs
Normal file
@ -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),
|
||||
))
|
||||
}
|
||||
}
|
3
pmg-rs/src/lib.rs
Normal file
3
pmg-rs/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod acme;
|
||||
pub mod apt;
|
||||
pub mod csr;
|
172
pmg-rs/test.pl
Normal file
172
pmg-rs/test.pl
Normal file
@ -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@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";
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user