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-*" ]
|
exclude = [ "build", "perl-*" ]
|
||||||
members = [
|
members = [
|
||||||
"pve-rs",
|
"pve-rs",
|
||||||
|
"pmg-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
7
Makefile
7
Makefile
@ -28,14 +28,15 @@ build:
|
|||||||
echo system >build/rust-toolchain
|
echo system >build/rust-toolchain
|
||||||
cp -a ./perl-* ./build/
|
cp -a ./perl-* ./build/
|
||||||
cp -a ./pve-rs ./build
|
cp -a ./pve-rs ./build
|
||||||
|
cp -a ./pmg-rs ./build
|
||||||
|
|
||||||
pve-deb: build
|
pve-deb: build
|
||||||
cd ./build/pve-rs && dpkg-buildpackage -b -uc -us
|
cd ./build/pve-rs && dpkg-buildpackage -b -uc -us
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
# pmg-deb: build
|
pmg-deb: build
|
||||||
# cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
|
cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
|
||||||
# touch $@
|
touch $@
|
||||||
|
|
||||||
%-upload: %-deb
|
%-upload: %-deb
|
||||||
cd build; \
|
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