Merge branch 'malte/updater' into 'main'
Implement process to update certificates in the store See merge request husk-project/husk-milter!11
This commit is contained in:
Generated
+43
-3
@@ -99,7 +99,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"blake2",
|
"blake2",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -333,6 +333,17 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.0",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -447,6 +458,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1055,6 +1075,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1270,6 +1291,8 @@ dependencies = [
|
|||||||
"lettre",
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"reqwest",
|
||||||
"sequoia-cert-store",
|
"sequoia-cert-store",
|
||||||
"sequoia-directories",
|
"sequoia-directories",
|
||||||
"sequoia-keystore",
|
"sequoia-keystore",
|
||||||
@@ -1614,7 +1637,7 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2290,6 +2313,17 @@ dependencies = [
|
|||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -2328,6 +2362,12 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@@ -2940,7 +2980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ indymilter = "0.3.0"
|
|||||||
lettre = "0.11.22"
|
lettre = "0.11.22"
|
||||||
log = "0.4.30"
|
log = "0.4.30"
|
||||||
log4rs = "1.3.0"
|
log4rs = "1.3.0"
|
||||||
|
rand = "0.10.2"
|
||||||
|
# the reqwest version has to be the same sequoia-net reexports!
|
||||||
|
reqwest = { version = "0.12", features = ["socks"] }
|
||||||
sequoia-cert-store = "0.7.3"
|
sequoia-cert-store = "0.7.3"
|
||||||
sequoia-directories = "0.1.0"
|
sequoia-directories = "0.1.0"
|
||||||
sequoia-keystore = "0.7.2"
|
sequoia-keystore = "0.7.2"
|
||||||
|
|||||||
+49
-2
@@ -1,13 +1,60 @@
|
|||||||
#
|
#
|
||||||
# Husk config file
|
# Husk config file
|
||||||
#
|
#
|
||||||
connection = "localhost:3000"
|
|
||||||
|
|
||||||
|
# The directory for anything Sequoia PGP related (keystore, certstore).
|
||||||
|
#
|
||||||
|
# default: /var/lib/husk
|
||||||
sequoia_home = "/tmp/sq_home"
|
sequoia_home = "/tmp/sq_home"
|
||||||
|
|
||||||
#keyservers = [ "hkps://keys.openpgp.org/", "hkps://mail-api.proton.me", "hkps://keys.mailvelope.com", "hkps://keyserver.ubuntu.com", "hkps://sks.pod01.fleetstreetops.com" ]
|
[milter]
|
||||||
|
# IP and port the milter will listen on.
|
||||||
|
#
|
||||||
|
# no default, must be specified.
|
||||||
|
connection = "localhost:3000"
|
||||||
|
|
||||||
|
# Keyservers to use for key lookup.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# keyservers = [ "hkps://keys.openpgp.org/", "hkps://mail-api.proton.me", "hkps://keys.mailvelope.com", "hkps://keyserver.ubuntu.com", "hkps://sks.pod01.fleetstreetops.com" ]
|
||||||
|
#
|
||||||
|
# The default is the empty list, which prevents keyserver lookups.
|
||||||
keyservers = [ "hkp://example.com" ]
|
keyservers = [ "hkp://example.com" ]
|
||||||
|
|
||||||
|
# Location of the pid file of the milter.
|
||||||
|
#
|
||||||
|
# default: /var/lib/husk/husk.pid
|
||||||
|
|
||||||
|
# Subject header lines are protected (exchanged).
|
||||||
|
#
|
||||||
|
# Specify your replacement string:
|
||||||
|
# subject_replacement = "Encrypted message"
|
||||||
|
#
|
||||||
|
# default: "..."
|
||||||
|
|
||||||
|
# Tor usage: If configured, the milter will use Tor via the specified
|
||||||
|
# sock5 proxy.
|
||||||
|
#
|
||||||
|
# use_tor_via = "socks5://localhost:9050/"
|
||||||
|
#
|
||||||
|
# default: unconfigured, meaning: no usage of Tor.
|
||||||
|
|
||||||
|
[updater]
|
||||||
|
|
||||||
|
# The interval between two update requests for a certificate are
|
||||||
|
# random.
|
||||||
|
#
|
||||||
|
# wait_min and wait_max specify the range for that randomness.
|
||||||
|
# defaults:
|
||||||
|
# wait_min = 5
|
||||||
|
# wait_max = 3600
|
||||||
|
wait_min = 1
|
||||||
|
wait_max = 2
|
||||||
|
|
||||||
|
# Tor usage of the updater process. Semantics (and default) are the same
|
||||||
|
# as with the milter.
|
||||||
|
use_tor_via = "socks5://localhost:9050/"
|
||||||
|
|
||||||
[logging.appenders.console]
|
[logging.appenders.console]
|
||||||
kind = "console"
|
kind = "console"
|
||||||
[logging.appenders.console.encoder]
|
[logging.appenders.console.encoder]
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
appenders:
|
|
||||||
stdout_appender:
|
|
||||||
kind: console
|
|
||||||
encoder:
|
|
||||||
pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
|
|
||||||
|
|
||||||
file_appender:
|
|
||||||
kind: file
|
|
||||||
path: "/tmp/husk.log"
|
|
||||||
encoder:
|
|
||||||
pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
|
|
||||||
|
|
||||||
root:
|
|
||||||
level: debug
|
|
||||||
appenders:
|
|
||||||
- stdout_appender
|
|
||||||
@@ -61,7 +61,8 @@ RUN apt-get update -yqq && \
|
|||||||
sq \
|
sq \
|
||||||
bind9-host \
|
bind9-host \
|
||||||
inotify-tools \
|
inotify-tools \
|
||||||
mailutils
|
mailutils \
|
||||||
|
tor
|
||||||
|
|
||||||
RUN adduser --shell /bin/bash --disabled-password --gecos "" hockeypuck
|
RUN adduser --shell /bin/bash --disabled-password --gecos "" hockeypuck
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ authenticate $KARL_FPR
|
|||||||
create_unix_user laura
|
create_unix_user laura
|
||||||
|
|
||||||
echo -e "To: karl@example.com, Laura <laura@example.com>\nSubject: Testmail\n\nTestmail." | sendmail -t
|
echo -e "To: karl@example.com, Laura <laura@example.com>\nSubject: Testmail\n\nTestmail." | sendmail -t
|
||||||
sleep 1
|
sleep 2
|
||||||
|
|
||||||
R=1
|
R=1
|
||||||
if [ $(expect_mail karl encrypted) == 0 ] ; then
|
if [ $(expect_mail karl encrypted) == 0 ] ; then
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub enum HuskSubcommands {
|
|||||||
Introducer(IntroducerCommand),
|
Introducer(IntroducerCommand),
|
||||||
Locals(LocalsCommand),
|
Locals(LocalsCommand),
|
||||||
Signkey(SignkeyCommand),
|
Signkey(SignkeyCommand),
|
||||||
|
Updater(UpdaterCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daemon subcommands
|
// Daemon subcommands
|
||||||
@@ -72,6 +73,42 @@ pub struct DaemonStartCommand {
|
|||||||
background: bool,
|
background: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updater subcommands
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(
|
||||||
|
name = "updater",
|
||||||
|
about = "Manage the updater process.",
|
||||||
|
subcommand_required = true,
|
||||||
|
arg_required_else_help = true,
|
||||||
|
disable_colored_help = true,
|
||||||
|
disable_version_flag = true,
|
||||||
|
)]
|
||||||
|
pub struct UpdaterCommand {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub subcommand: UpdaterSubcommand,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum UpdaterSubcommand {
|
||||||
|
Start(UpdaterStartCommand),
|
||||||
|
Stop,
|
||||||
|
Status
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(
|
||||||
|
name = "daemon",
|
||||||
|
about = "Start the updater.",
|
||||||
|
)]
|
||||||
|
pub struct UpdaterStartCommand {
|
||||||
|
#[clap(
|
||||||
|
long = "background",
|
||||||
|
help = "Start the updater as a background process.",
|
||||||
|
default_value_t = false,
|
||||||
|
)]
|
||||||
|
background: bool,
|
||||||
|
}
|
||||||
|
|
||||||
// Introducer subcommands
|
// Introducer subcommands
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::cli::cli_args::{CliArgs, HuskSubcommands};
|
|||||||
use crate::config::HuskConfigContainer;
|
use crate::config::HuskConfigContainer;
|
||||||
|
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
|
pub mod updater;
|
||||||
pub mod introducer;
|
pub mod introducer;
|
||||||
pub mod locals;
|
pub mod locals;
|
||||||
pub mod signkey;
|
pub mod signkey;
|
||||||
@@ -48,6 +49,9 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> {
|
|||||||
HuskSubcommands::Signkey(subcmd) => {
|
HuskSubcommands::Signkey(subcmd) => {
|
||||||
commands::signkey::dispatch(subcmd, config).await?;
|
commands::signkey::dispatch(subcmd, config).await?;
|
||||||
}
|
}
|
||||||
|
HuskSubcommands::Updater(subcmd) => {
|
||||||
|
commands::updater::dispatch(subcmd, config).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::cli::cli_args::{DaemonCommand, DaemonSubcommand};
|
|||||||
use crate::common::process_descr::ProcessDescr;
|
use crate::common::process_descr::ProcessDescr;
|
||||||
use crate::config::HuskConfigContainer;
|
use crate::config::HuskConfigContainer;
|
||||||
use crate::config::HuskConfig;
|
use crate::config::HuskConfig;
|
||||||
use crate::Daemon;
|
use crate::daemon::Daemon;
|
||||||
|
|
||||||
pub async fn dispatch(cmd: DaemonCommand, config: HuskConfigContainer) -> Result<()> {
|
pub async fn dispatch(cmd: DaemonCommand, config: HuskConfigContainer) -> Result<()> {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//! Updater first level dispatcher
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::cli::cli_args::{UpdaterCommand, UpdaterSubcommand};
|
||||||
|
use crate::common::process_descr::ProcessDescr;
|
||||||
|
use crate::config::HuskConfigContainer;
|
||||||
|
use crate::config::HuskConfig;
|
||||||
|
use crate::updater::Updater;
|
||||||
|
|
||||||
|
pub async fn dispatch(cmd: UpdaterCommand, config: HuskConfigContainer) -> Result<()> {
|
||||||
|
|
||||||
|
let c: HuskConfig = config.clone().into();
|
||||||
|
|
||||||
|
match cmd.subcommand {
|
||||||
|
UpdaterSubcommand::Start(_sub_cmd) => {
|
||||||
|
// Write the pid file.
|
||||||
|
let pid_file_name = c.get_updater_pid_file();
|
||||||
|
ProcessDescr::create(pid_file_name.clone())?;
|
||||||
|
|
||||||
|
let result_code = Updater::run(config).await;
|
||||||
|
|
||||||
|
// and remove it when the daemon terminated.
|
||||||
|
let _ = ProcessDescr::remove(pid_file_name);
|
||||||
|
return result_code;
|
||||||
|
},
|
||||||
|
UpdaterSubcommand::Stop => {
|
||||||
|
let process_descr = ProcessDescr::load(c.get_updater_pid_file())?;
|
||||||
|
if process_descr.send_sig_interrupt().is_ok() {
|
||||||
|
let _ = ProcessDescr::remove(process_descr.file_location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UpdaterSubcommand::Status => {
|
||||||
|
if let Ok(process_descr) = ProcessDescr::load(c.get_updater_pid_file()) {
|
||||||
|
if process_descr.is_running() {
|
||||||
|
println!("Running, with pid: {}", process_descr.pid);
|
||||||
|
} else {
|
||||||
|
println!("Not running, but stale pid file detected: {}",
|
||||||
|
c.get_updater_pid_file());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Not running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
+120
-41
@@ -22,6 +22,7 @@ use sequoia_openpgp::packet::prelude::SignatureBuilder;
|
|||||||
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
||||||
use sequoia_openpgp::policy::StandardPolicy;
|
use sequoia_openpgp::policy::StandardPolicy;
|
||||||
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, Signer, LiteralWriter};
|
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, Signer, LiteralWriter};
|
||||||
|
use sequoia_openpgp::serialize::stream::padding::Padder;
|
||||||
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
||||||
use sequoia_openpgp::types::SignatureType;
|
use sequoia_openpgp::types::SignatureType;
|
||||||
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
||||||
@@ -30,6 +31,7 @@ use sequoia_wot::{self as wot, CertSynopsis};
|
|||||||
use wot::store::Store as _;
|
use wot::store::Store as _;
|
||||||
use wot::{Depth, Path};
|
use wot::{Depth, Path};
|
||||||
|
|
||||||
|
use crate::config::TorConnectorType;
|
||||||
use crate::types::errors::HuskError;
|
use crate::types::errors::HuskError;
|
||||||
use crate::types::husk_context::HuskContext;
|
use crate::types::husk_context::HuskContext;
|
||||||
use crate::types::introducer::Introducer;
|
use crate::types::introducer::Introducer;
|
||||||
@@ -96,40 +98,80 @@ const USER_AGENT: &'static str = concat!("husk/", env!("CARGO_PKG_VERSION"));
|
|||||||
const CONNECT_TIMEOUT: Duration = Duration::new(5, 0);
|
const CONNECT_TIMEOUT: Duration = Duration::new(5, 0);
|
||||||
const REQUEST_TIMEOUT: Duration = Duration::new(5, 0);
|
const REQUEST_TIMEOUT: Duration = Duration::new(5, 0);
|
||||||
|
|
||||||
/// Try to fetch certificates for `email` from online sources (keyservers, wkd,
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum OnlineQuery {
|
||||||
|
Fingerprint(Fingerprint),
|
||||||
|
Email(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to fetch certificates for email from online sources (keyservers, wkd,
|
||||||
/// dane).
|
/// dane).
|
||||||
pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
pub async fn lookup_email<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||||
|
-> anyhow::Result<Vec<Cert>> {
|
||||||
|
query_online_sources(context, OnlineQuery::Email(email.into())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to fetch certificates for fingerprint from keyservers. wkd
|
||||||
|
/// and dane can only be queried for email addresses.
|
||||||
|
pub async fn lookup_fingerprint<'hc>(context: &HuskContext<'hc>, fpr: &str)
|
||||||
|
-> anyhow::Result<Vec<Cert>> {
|
||||||
|
let fpr = fpr.parse::<Fingerprint>()?;
|
||||||
|
query_online_sources(context, OnlineQuery::Fingerprint(fpr)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to fetch certificates for query from online sources (keyservers, wkd,
|
||||||
|
/// dane).
|
||||||
|
async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuery)
|
||||||
-> anyhow::Result<Vec<Cert>> {
|
-> anyhow::Result<Vec<Cert>> {
|
||||||
|
|
||||||
log::debug!("remote lookup for {}", email);
|
log::debug!("remote lookup for {:?}", query);
|
||||||
|
|
||||||
let mut jobs = JoinSet::new();
|
let mut jobs = JoinSet::new();
|
||||||
let http_client = sequoia_net::reqwest::Client::builder()
|
let mut http_client_builder = sequoia_net::reqwest::Client::builder()
|
||||||
.user_agent(USER_AGENT)
|
.user_agent(USER_AGENT)
|
||||||
.connect_timeout(CONNECT_TIMEOUT)
|
.connect_timeout(CONNECT_TIMEOUT)
|
||||||
.timeout(REQUEST_TIMEOUT)
|
.timeout(REQUEST_TIMEOUT);
|
||||||
.build()?;
|
|
||||||
|
if let TorConnectorType::SocksProxy(socks_url) = &context.use_tor_via {
|
||||||
|
// Configure the use of a tor proxy.
|
||||||
|
let mut nonce: [u8; 4] = [0; 4];
|
||||||
|
sequoia_openpgp::crypto::random(&mut nonce[..])?;
|
||||||
|
let nonce: String = sequoia_openpgp::fmt::hex::encode(&nonce);
|
||||||
|
let url: String = format!("socks5h://anonymous:{}@{}", nonce, socks_url);
|
||||||
|
|
||||||
|
http_client_builder = http_client_builder
|
||||||
|
.proxy(sequoia_net::reqwest::Proxy::all(url)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let http_client = http_client_builder.build()?;
|
||||||
|
|
||||||
// local function to check certificates for usability
|
// local function to check certificates for usability
|
||||||
let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, email: String, policy: &StandardPolicy| {
|
let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, query: OnlineQuery, policy: &StandardPolicy| {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut seen: HashSet<Fingerprint> = HashSet::new();
|
let mut seen: HashSet<Fingerprint> = HashSet::new();
|
||||||
|
|
||||||
for cert in certs.into_iter().flatten() {
|
for cert in certs.into_iter().flatten() {
|
||||||
if let Ok(vc) = cert.with_policy(policy, None) {
|
if let Ok(vc) = cert.with_policy(policy, None) {
|
||||||
|
|
||||||
// check if the returned certificate contains a userid with the
|
let mut found = false;
|
||||||
// email address in question.
|
match query.clone() {
|
||||||
let mut userid_found = false;
|
OnlineQuery::Email(email) => {
|
||||||
for userid in vc.userids() {
|
// check if the returned certificate contains a userid with the
|
||||||
if let Ok(Some(u)) = userid.userid().email() {
|
// email address in question.
|
||||||
if u == email {
|
for userid in vc.userids() {
|
||||||
userid_found = true;
|
if let Ok(Some(u)) = userid.userid().email() {
|
||||||
|
if u == email {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
OnlineQuery::Fingerprint(f) => {
|
||||||
|
found = cert.fingerprint() == f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if userid_found {
|
if found {
|
||||||
// check if the certificate can be used (for encryption).
|
// check if the certificate can be used (for encryption).
|
||||||
vc.keys()
|
vc.keys()
|
||||||
.supported()
|
.supported()
|
||||||
@@ -156,43 +198,72 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
|||||||
|
|
||||||
if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) {
|
if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) {
|
||||||
|
|
||||||
let e = email.to_string();
|
|
||||||
let p = StandardPolicy::new();
|
let p = StandardPolicy::new();
|
||||||
|
let query = query.clone();
|
||||||
|
|
||||||
jobs.spawn(async move {
|
jobs.spawn(async move {
|
||||||
let mut partial_result = Vec::new();
|
let mut partial_result = Vec::new();
|
||||||
|
|
||||||
if let Ok(certs) = ks.search(e.as_str()).await {
|
let q = query.clone();
|
||||||
partial_result = collect_certs(certs, e, &p);
|
match query {
|
||||||
|
OnlineQuery::Email(e) => {
|
||||||
|
match ks.search(e.as_str()).await {
|
||||||
|
Ok(certs) => {
|
||||||
|
partial_result = collect_certs(certs, q, &p);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::debug!("Error: {} {}", e, err.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if let Ok(certs) = ks.search(e.as_str()).await {
|
||||||
|
partial_result = collect_certs(certs, q, &p);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
partial_result
|
||||||
|
},
|
||||||
|
OnlineQuery::Fingerprint(f) => {
|
||||||
|
if let Ok(certs) = ks.get(f).await {
|
||||||
|
partial_result = collect_certs(certs, q, &p);
|
||||||
|
}
|
||||||
|
partial_result
|
||||||
|
},
|
||||||
}
|
}
|
||||||
partial_result
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WKD
|
// WKD and DANE need an email address
|
||||||
let e = email.to_string();
|
if let OnlineQuery::Email(_) = query {
|
||||||
let p = StandardPolicy::new();
|
let p = StandardPolicy::new();
|
||||||
jobs.spawn(async move {
|
let q = query.clone();
|
||||||
let mut partial_result = Vec::new();
|
|
||||||
|
|
||||||
if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await {
|
// WKD
|
||||||
partial_result = collect_certs(certs, e, &p);
|
jobs.spawn(async move {
|
||||||
}
|
let mut partial_result = Vec::new();
|
||||||
partial_result
|
|
||||||
});
|
|
||||||
|
|
||||||
// DANE
|
if let OnlineQuery::Email(e) = q.clone() {
|
||||||
let e = email.to_string();
|
if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await {
|
||||||
let p = StandardPolicy::new();
|
partial_result = collect_certs(certs, q, &p);
|
||||||
jobs.spawn(async move {
|
}
|
||||||
let mut partial_result = Vec::new();
|
}
|
||||||
|
partial_result
|
||||||
|
});
|
||||||
|
|
||||||
if let Ok(certs) = dane::get(e.as_str()).await {
|
// DANE
|
||||||
partial_result = collect_certs(certs, e, &p);
|
let p = StandardPolicy::new();
|
||||||
}
|
let q = query.clone();
|
||||||
partial_result
|
jobs.spawn(async move {
|
||||||
});
|
let mut partial_result = Vec::new();
|
||||||
|
|
||||||
|
if let OnlineQuery::Email(e) = q {
|
||||||
|
if let Ok(certs) = dane::get(e.as_str()).await {
|
||||||
|
partial_result = collect_certs(certs, query, &p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
partial_result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// join the partial results. As the same certificate might be returned by
|
// join the partial results. As the same certificate might be returned by
|
||||||
// several sources, dedup and merge the certificates.
|
// several sources, dedup and merge the certificates.
|
||||||
@@ -240,7 +311,7 @@ pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
|||||||
introducer.cert.self_signed_userids().map(|u| u.userid()).collect::<Vec<_>>());
|
introducer.cert.self_signed_userids().map(|u| u.userid()).collect::<Vec<_>>());
|
||||||
if introducer.can_introduce(format!("<{}>", email).as_str()) {
|
if introducer.can_introduce(format!("<{}>", email).as_str()) {
|
||||||
log::debug!(" introducer regex matches!");
|
log::debug!(" introducer regex matches!");
|
||||||
match lookup_certificates(context, email).await {
|
match lookup_email(context, email).await {
|
||||||
Ok(certs) => {
|
Ok(certs) => {
|
||||||
for cert in certs {
|
for cert in certs {
|
||||||
if introducer.is_introducing(&cert) {
|
if introducer.is_introducing(&cert) {
|
||||||
@@ -412,9 +483,14 @@ pub fn is_encrypted(body: &Bytes) -> bool {
|
|||||||
pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipient>)
|
pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipient>)
|
||||||
-> anyhow::Result<Bytes> {
|
-> anyhow::Result<Bytes> {
|
||||||
|
|
||||||
|
log::debug!("encrypt for {}", recipients.iter()
|
||||||
|
.map(|r| r.email.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", "));
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
|
|
||||||
for rcpt in recipients {
|
for rcpt in recipients {
|
||||||
|
log::debug!("Check recipient {}", rcpt.email);
|
||||||
if rcpt.can_encrypt() {
|
if rcpt.can_encrypt() {
|
||||||
for cert in &rcpt.certs {
|
for cert in &rcpt.certs {
|
||||||
cert.with_policy(&context.policy, None)?
|
cert.with_policy(&context.policy, None)?
|
||||||
@@ -434,6 +510,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
|||||||
let message = Message::new(&mut sink);
|
let message = Message::new(&mut sink);
|
||||||
let message = Armorer::new(message).build()?;
|
let message = Armorer::new(message).build()?;
|
||||||
let mut message = Encryptor::for_recipients(message, keys).build()?;
|
let mut message = Encryptor::for_recipients(message, keys).build()?;
|
||||||
|
message = Padder::new(message).build()?;
|
||||||
if let Some(key) = &context.signing_key {
|
if let Some(key) = &context.signing_key {
|
||||||
log::debug!("use signing key {}", key.fingerprint());
|
log::debug!("use signing key {}", key.fingerprint());
|
||||||
message = Signer::new(message, key.clone())?.build()?;
|
message = Signer::new(message, key.clone())?.build()?;
|
||||||
@@ -451,6 +528,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
|||||||
|
|
||||||
Ok(Bytes::from(sink))
|
Ok(Bytes::from(sink))
|
||||||
} else {
|
} else {
|
||||||
|
log::debug!("not keys at all");
|
||||||
Err(HuskError::NoEncryptionKeys.into())
|
Err(HuskError::NoEncryptionKeys.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -887,7 +965,8 @@ jk6lh01Ej9QTGQ==
|
|||||||
introducers: Vec::new(),
|
introducers: Vec::new(),
|
||||||
keyservers: Vec::new(),
|
keyservers: Vec::new(),
|
||||||
subject_replacement: "...".into(),
|
subject_replacement: "...".into(),
|
||||||
mail: MailContext::new()
|
mail: MailContext::new(),
|
||||||
|
use_tor_via: crate::config::TorConnectorType::NotUsed,
|
||||||
};
|
};
|
||||||
|
|
||||||
// store the local trust root
|
// store the local trust root
|
||||||
|
|||||||
+138
-17
@@ -9,17 +9,65 @@ use toml;
|
|||||||
use log4rs;
|
use log4rs;
|
||||||
|
|
||||||
use crate::types::errors::HuskError;
|
use crate::types::errors::HuskError;
|
||||||
|
use crate::types::defaults::CONNECTION;
|
||||||
use crate::types::defaults::CONSOLE_LOGGING;
|
use crate::types::defaults::CONSOLE_LOGGING;
|
||||||
|
use crate::types::defaults::UPDATER_PID_FILE_LOCATION;
|
||||||
|
use crate::types::defaults::UPDATER_STATE_FILE_LOCATION;
|
||||||
|
use crate::types::defaults::UPDATER_WAIT_MIN;
|
||||||
|
use crate::types::defaults::UPDATER_WAIT_MAX;
|
||||||
use crate::types::defaults::PID_FILE_LOCATION;
|
use crate::types::defaults::PID_FILE_LOCATION;
|
||||||
use crate::types::defaults::SUBJECT_REPLACEMENT;
|
use crate::types::defaults::SUBJECT_REPLACEMENT;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct HuskConfig {
|
pub enum TorConnectorType {
|
||||||
|
NotUsed,
|
||||||
|
Errornous,
|
||||||
|
SocksProxy(String),
|
||||||
|
Arti,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<String>> for TorConnectorType {
|
||||||
|
fn from(value: Option<String>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some(v) => {
|
||||||
|
if v.eq_ignore_ascii_case("arti") {
|
||||||
|
TorConnectorType::Arti
|
||||||
|
} else {
|
||||||
|
if let Some(url) = v.strip_prefix("socks5://") {
|
||||||
|
TorConnectorType::SocksProxy(url.to_string())
|
||||||
|
} else {
|
||||||
|
TorConnectorType::Errornous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => TorConnectorType::NotUsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct MilterConfig {
|
||||||
pub connection: String,
|
pub connection: String,
|
||||||
pub sequoia_home: String,
|
pub pid_file: Option<String>,
|
||||||
pub keyservers: Option<Vec<String>>,
|
pub keyservers: Option<Vec<String>>,
|
||||||
pub subject_replacement: Option<String>,
|
pub subject_replacement: Option<String>,
|
||||||
|
use_tor_via: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct UpdaterConfig {
|
||||||
|
pub wait_min: Option<u32>,
|
||||||
|
pub wait_max: Option<u32>,
|
||||||
pub pid_file: Option<String>,
|
pub pid_file: Option<String>,
|
||||||
|
pub state_file: Option<String>,
|
||||||
|
use_tor_via: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct HuskConfig {
|
||||||
|
pub sequoia_home: String,
|
||||||
|
milter: Option<MilterConfig>,
|
||||||
|
updater: Option<UpdaterConfig>,
|
||||||
logging: Option<toml::Value>,
|
logging: Option<toml::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,26 +112,101 @@ impl HuskConfig {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_keyservers(&self) -> Vec<String> {
|
pub fn get_connection(&self) -> String {
|
||||||
if let Some(kss) = &self.keyservers {
|
if let Some(milter_config) = &self.milter {
|
||||||
kss.clone()
|
milter_config.connection.clone()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
CONNECTION.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_subject_replacement(&self) -> String {
|
pub fn get_milter_tor_usage(&self) -> TorConnectorType {
|
||||||
if let Some(replacement) = &self.subject_replacement {
|
if let Some(milter_config) = &self.milter {
|
||||||
replacement.clone()
|
milter_config.use_tor_via.clone().into()
|
||||||
} else {
|
} else {
|
||||||
SUBJECT_REPLACEMENT.to_string()
|
TorConnectorType::NotUsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_keyservers(&self) -> Vec<String> {
|
||||||
|
if let Some(milter_config) = &self.milter {
|
||||||
|
if let Some(kss) = &milter_config.keyservers {
|
||||||
|
return kss.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_subject_replacement(&self) -> String {
|
||||||
|
if let Some(milter_config) = &self.milter {
|
||||||
|
if let Some(replacement) = &milter_config.subject_replacement {
|
||||||
|
return replacement.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUBJECT_REPLACEMENT.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_pid_file(&self) -> String {
|
pub fn get_pid_file(&self) -> String {
|
||||||
if let Some(pid_file) = &self.pid_file {
|
if let Some(milter_config) = &self.milter {
|
||||||
pid_file.clone()
|
if let Some(pid_file) = &milter_config.pid_file {
|
||||||
|
return pid_file.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PID_FILE_LOCATION.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_updater_pid_file(&self) -> String {
|
||||||
|
if let Some(updater_config) = &self.updater {
|
||||||
|
if let Some(pid_file) = &updater_config.pid_file {
|
||||||
|
pid_file.clone()
|
||||||
|
} else {
|
||||||
|
UPDATER_PID_FILE_LOCATION.to_string()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
PID_FILE_LOCATION.to_string()
|
UPDATER_PID_FILE_LOCATION.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_updater_state_file(&self) -> String {
|
||||||
|
if let Some(updater_config) = &self.updater {
|
||||||
|
if let Some(state_file) = &updater_config.state_file {
|
||||||
|
state_file.clone()
|
||||||
|
} else {
|
||||||
|
UPDATER_STATE_FILE_LOCATION.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UPDATER_STATE_FILE_LOCATION.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_updater_min_wait(&self) -> u32 {
|
||||||
|
if let Some(updater_config) = &self.updater {
|
||||||
|
if let Some(wait_min) = &updater_config.wait_min {
|
||||||
|
*wait_min
|
||||||
|
} else {
|
||||||
|
UPDATER_WAIT_MIN
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UPDATER_WAIT_MIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_updater_max_wait(&self) -> u32 {
|
||||||
|
if let Some(updater_config) = &self.updater {
|
||||||
|
if let Some(wait_max) = &updater_config.wait_max {
|
||||||
|
*wait_max
|
||||||
|
} else {
|
||||||
|
UPDATER_WAIT_MAX
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UPDATER_WAIT_MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_updater_tor_usage(&self) -> TorConnectorType {
|
||||||
|
if let Some(updater_config) = &self.updater {
|
||||||
|
updater_config.use_tor_via.clone().into()
|
||||||
|
} else {
|
||||||
|
TorConnectorType::NotUsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,12 +230,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn subject_line() {
|
fn subject_line() {
|
||||||
let config = HuskConfig {
|
let config = HuskConfig {
|
||||||
connection: "".into(),
|
|
||||||
sequoia_home: "".into(),
|
sequoia_home: "".into(),
|
||||||
keyservers: None,
|
|
||||||
subject_replacement: None,
|
|
||||||
logging: None,
|
logging: None,
|
||||||
pid_file: None,
|
milter: None,
|
||||||
|
updater: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT);
|
assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT);
|
||||||
|
|||||||
+2
-1
@@ -33,7 +33,7 @@ impl Daemon {
|
|||||||
|
|
||||||
let config: HuskConfig = config_container.clone().into();
|
let config: HuskConfig = config_container.clone().into();
|
||||||
|
|
||||||
let listener = TcpListener::bind(&config.connection)
|
let listener = TcpListener::bind(&config.get_connection())
|
||||||
.await
|
.await
|
||||||
.expect(" opening milter socket");
|
.expect(" opening milter socket");
|
||||||
|
|
||||||
@@ -116,6 +116,7 @@ impl Daemon {
|
|||||||
// setup Context
|
// setup Context
|
||||||
match HuskContext::new(&config) {
|
match HuskContext::new(&config) {
|
||||||
Ok(mut context) => {
|
Ok(mut context) => {
|
||||||
|
context.for_milter(&config);
|
||||||
context.set_sender(sender.unwrap());
|
context.set_sender(sender.unwrap());
|
||||||
cx.data = Some(context);
|
cx.data = Some(context);
|
||||||
Status::Continue
|
Status::Continue
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,7 @@ use config::{HuskConfig, HuskConfigContainer};
|
|||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
use daemon::Daemon;
|
pub mod updater;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
||||||
pub fn main() -> anyhow::Result<()> {
|
pub fn main() -> anyhow::Result<()> {
|
||||||
@@ -26,6 +26,7 @@ pub fn main() -> anyhow::Result<()> {
|
|||||||
// XXX keep this in sync with cli/cli_args.rs
|
// XXX keep this in sync with cli/cli_args.rs
|
||||||
for arg in std::env::args() {
|
for arg in std::env::args() {
|
||||||
if arg == "daemon" && daemonize == 0 { daemonize = 1; }
|
if arg == "daemon" && daemonize == 0 { daemonize = 1; }
|
||||||
|
if arg == "updater" && daemonize == 0 { daemonize = 1; }
|
||||||
if arg == "start" && daemonize == 1 { daemonize = 2; }
|
if arg == "start" && daemonize == 1 { daemonize = 2; }
|
||||||
if arg == "--background" && daemonize == 2 { daemonize = 3; }
|
if arg == "--background" && daemonize == 2 { daemonize = 3; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
//! Defaults
|
//! Defaults
|
||||||
|
|
||||||
|
/// Default host and port of the milter.
|
||||||
|
pub const CONNECTION: &str = "localhost:3000";
|
||||||
|
|
||||||
pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml";
|
pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml";
|
||||||
|
|
||||||
/// If encrypting a mail, the subject is moved into the encrypted body
|
/// If encrypting a mail, the subject is moved into the encrypted body
|
||||||
/// and replaced by `SUBJECT_REPLACEMENT` (unless configured otherwise).
|
/// and replaced by `SUBJECT_REPLACEMENT` (unless configured otherwise).
|
||||||
pub const SUBJECT_REPLACEMENT: &str = "...";
|
pub const SUBJECT_REPLACEMENT: &str = "...";
|
||||||
|
|
||||||
pub const PID_FILE_LOCATION: &str = "/run/husk/pid";
|
pub const PID_FILE_LOCATION: &str = "/var/lib/husk/husk.pid";
|
||||||
|
pub const UPDATER_PID_FILE_LOCATION: &str = "/var/lib/husk/updater.pid";
|
||||||
|
pub const UPDATER_STATE_FILE_LOCATION: &str = "/var/lib/husk/updater.state";
|
||||||
|
|
||||||
|
pub const UPDATER_WAIT_MIN: u32 = 5;
|
||||||
|
pub const UPDATER_WAIT_MAX: u32 = 3600;
|
||||||
|
|
||||||
pub const FOOTER: &str = "---\nProcessed by Husk at {date}.\n";
|
pub const FOOTER: &str = "---\nProcessed by Husk at {date}.\n";
|
||||||
pub const FOOTER_RECIPIENTS: &str = "This mail was also send to {rcpts}.\n";
|
pub const FOOTER_RECIPIENTS: &str = "This mail was also send to {rcpts}.\n";
|
||||||
|
|||||||
+34
-11
@@ -17,7 +17,7 @@ use sequoia_cert_store::CertStore;
|
|||||||
use sequoia_directories::Home;
|
use sequoia_directories::Home;
|
||||||
use sequoia_keystore::Key;
|
use sequoia_keystore::Key;
|
||||||
|
|
||||||
use crate::config::HuskConfig;
|
use crate::config::{HuskConfig, TorConnectorType};
|
||||||
use crate::common::crypto;
|
use crate::common::crypto;
|
||||||
use crate::types::mail_context::MailContext;
|
use crate::types::mail_context::MailContext;
|
||||||
|
|
||||||
@@ -32,12 +32,22 @@ pub struct HuskContext<'hc> {
|
|||||||
pub keyservers: Vec<String>,
|
pub keyservers: Vec<String>,
|
||||||
pub subject_replacement: String,
|
pub subject_replacement: String,
|
||||||
pub mail: MailContext<'hc>,
|
pub mail: MailContext<'hc>,
|
||||||
|
pub use_tor_via: TorConnectorType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'hc> HuskContext<'hc> {
|
impl<'hc> HuskContext<'hc> {
|
||||||
|
|
||||||
/// Create a new HuskContext instance.
|
|
||||||
pub fn new(config: &HuskConfig) -> anyhow::Result<HuskContext<'hc>> {
|
pub fn new(config: &HuskConfig) -> anyhow::Result<HuskContext<'hc>> {
|
||||||
|
Self::create(config, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new HuskContext without determine the signing key
|
||||||
|
pub fn without_signkey(config: &HuskConfig) -> anyhow::Result<HuskContext<'hc>> {
|
||||||
|
Self::create(config, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new HuskContext instance.
|
||||||
|
pub fn create(config: &HuskConfig, with_signkey: bool) -> anyhow::Result<HuskContext<'hc>> {
|
||||||
|
|
||||||
let sequoia_home = Home::new(PathBuf::from(&config.sequoia_home))?;
|
let sequoia_home = Home::new(PathBuf::from(&config.sequoia_home))?;
|
||||||
let cert_store_base = sequoia_home.data_dir(sequoia_directories::Component::CertD);
|
let cert_store_base = sequoia_home.data_dir(sequoia_directories::Component::CertD);
|
||||||
@@ -48,16 +58,20 @@ impl<'hc> HuskContext<'hc> {
|
|||||||
let local_trust_root = crypto::get_local_trust_root(&cert_store)?;
|
let local_trust_root = crypto::get_local_trust_root(&cert_store)?;
|
||||||
let introducers = crypto::get_introducers(&cert_store, &policy, local_trust_root.fingerprint());
|
let introducers = crypto::get_introducers(&cert_store, &policy, local_trust_root.fingerprint());
|
||||||
|
|
||||||
let signing_key = match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) {
|
let signing_key = if with_signkey {
|
||||||
Ok(Some(cert)) => Some(cert),
|
match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) {
|
||||||
Ok(None) => {
|
Ok(Some(cert)) => Some(cert),
|
||||||
log::warn!("No signing key found");
|
Ok(None) => {
|
||||||
None
|
log::warn!("No signing key found");
|
||||||
},
|
None
|
||||||
Err(e) => {
|
},
|
||||||
log::error!("Error while fetching signing key: {}", e);
|
Err(e) => {
|
||||||
None
|
log::error!("Error while fetching signing key: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HuskContext {
|
Ok(HuskContext {
|
||||||
@@ -69,6 +83,7 @@ impl<'hc> HuskContext<'hc> {
|
|||||||
keyservers: config.get_keyservers(),
|
keyservers: config.get_keyservers(),
|
||||||
subject_replacement: config.get_subject_replacement(),
|
subject_replacement: config.get_subject_replacement(),
|
||||||
mail: MailContext::new(),
|
mail: MailContext::new(),
|
||||||
|
use_tor_via: TorConnectorType::NotUsed,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,4 +111,12 @@ impl<'hc> HuskContext<'hc> {
|
|||||||
self.mail.add_recipient(rcpt, certs)
|
self.mail.add_recipient(rcpt, certs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn for_milter(&mut self, config: &HuskConfig) {
|
||||||
|
self.use_tor_via = config.get_milter_tor_usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_updater(&mut self, config: &HuskConfig) {
|
||||||
|
self.use_tor_via = config.get_updater_tor_usage();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
//! Husk certificate updater
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sequoia_cert_store::LazyCert;
|
||||||
|
use sequoia_cert_store::Store;
|
||||||
|
use sequoia_cert_store::StoreUpdate;
|
||||||
|
use sequoia_cert_store::store::MergePublicCollectStats;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tokio::time::Duration;
|
||||||
|
|
||||||
|
use crate::config::HuskConfig;
|
||||||
|
use crate::config::HuskConfigContainer;
|
||||||
|
use crate::common::crypto;
|
||||||
|
use crate::types::husk_context::HuskContext;
|
||||||
|
use state::UpdaterState;
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
pub struct Updater { }
|
||||||
|
|
||||||
|
impl Updater {
|
||||||
|
|
||||||
|
async fn delay(min: u32, max: u32) {
|
||||||
|
|
||||||
|
let val = rand::random_range(min..max);
|
||||||
|
log::debug!("updater sleeping for {} secs", val);
|
||||||
|
sleep(Duration::from_secs(val as u64)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(config_container: HuskConfigContainer) -> anyhow::Result<()> {
|
||||||
|
|
||||||
|
let c: HuskConfig = config_container.into();
|
||||||
|
|
||||||
|
let mut stay = true;
|
||||||
|
let state_file = c.get_updater_state_file();
|
||||||
|
let state_path = Path::new(&state_file);
|
||||||
|
while stay {
|
||||||
|
Self::delay(c.get_updater_min_wait(), c.get_updater_max_wait()).await;
|
||||||
|
|
||||||
|
let mut updater_state = UpdaterState::thaw(state_path)
|
||||||
|
.await
|
||||||
|
.inspect_err(|_| { stay = false; })?;
|
||||||
|
let mut context = HuskContext::without_signkey(&c)?;
|
||||||
|
context.for_updater(&c);
|
||||||
|
updater_state.refresh(&context);
|
||||||
|
|
||||||
|
if let Some(fpr) = updater_state.next() {
|
||||||
|
log::debug!("try to update {}", fpr.to_hex());
|
||||||
|
|
||||||
|
// get local cert from cert store to get the user emails
|
||||||
|
let mut emails = Vec::new();
|
||||||
|
if let Ok(local_certs) = context.cert_store.lookup_by_cert(&fpr.clone().into()) {
|
||||||
|
local_certs.iter()
|
||||||
|
.map(|c| c.with_policy(&context.policy, None))
|
||||||
|
.filter_map(|c| c.ok())
|
||||||
|
.filter(|c|
|
||||||
|
c.keys()
|
||||||
|
.supported()
|
||||||
|
.alive()
|
||||||
|
.revoked(false)
|
||||||
|
.for_transport_encryption()
|
||||||
|
.count() > 0)
|
||||||
|
.for_each(|c|
|
||||||
|
c.userids()
|
||||||
|
.map(|u| u.userid().email())
|
||||||
|
.filter_map(|u| if let Ok(e) = u { e } else { None })
|
||||||
|
.for_each(|e| emails.push(e.to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
log::debug!("with emails: {:?}", emails);
|
||||||
|
|
||||||
|
let stats = MergePublicCollectStats::new();
|
||||||
|
|
||||||
|
let mut certs = crypto::lookup_fingerprint(&context, fpr.to_hex().as_str()).await?;
|
||||||
|
for email in emails {
|
||||||
|
crypto::lookup_email(&context, email.as_str()).await?.iter()
|
||||||
|
.for_each(|c| {
|
||||||
|
if c.fingerprint() == fpr {
|
||||||
|
certs.push(c.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !certs.is_empty() {
|
||||||
|
for cert in certs {
|
||||||
|
let cert = Arc::new(LazyCert::from_cert(cert));
|
||||||
|
if context.cert_store.update_by(cert, &stats).is_ok() {
|
||||||
|
updater_state.mark_processed(&fpr, stats.updated_certs() > 0);
|
||||||
|
} else {
|
||||||
|
updater_state.incr_retry_count(&fpr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no updates found
|
||||||
|
updater_state.mark_processed(&fpr, false);
|
||||||
|
}
|
||||||
|
UpdaterState::freeze(state_path, &updater_state)
|
||||||
|
.await
|
||||||
|
.inspect_err(|_| { stay = false; })?;
|
||||||
|
} else {
|
||||||
|
// finished update round
|
||||||
|
let (processed, updated) = updater_state.summarize();
|
||||||
|
log::info!("Updater finished round: {} certificates processed, {} updated",
|
||||||
|
processed,
|
||||||
|
updated);
|
||||||
|
// remove state from disk
|
||||||
|
UpdaterState::remove(state_path)
|
||||||
|
.await
|
||||||
|
.inspect_err(|_| { stay = false; })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
//! manage the state of certificates while updating
|
||||||
|
|
||||||
|
use std::{fmt, fs};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
|
||||||
|
use chrono::{Local, NaiveDateTime};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use sequoia_openpgp::Fingerprint;
|
||||||
|
use sequoia_openpgp::types::RevocationStatus;
|
||||||
|
use sequoia_openpgp::parse::buffered_reader::File;
|
||||||
|
use sequoia_cert_store::Store;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::types::husk_context::HuskContext;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum UpdaterError {
|
||||||
|
#[error("IO Error {0}")]
|
||||||
|
IoError(String),
|
||||||
|
#[error("Start time missing")]
|
||||||
|
StartTimeMissing,
|
||||||
|
#[error("Wrong record size {0}")]
|
||||||
|
WrongRecordSize(usize),
|
||||||
|
#[error("Data missing")]
|
||||||
|
DataMissing,
|
||||||
|
#[error("Broken persisted cert state: {0}")]
|
||||||
|
BrokenState(String),
|
||||||
|
#[error("File not found {0}")]
|
||||||
|
FileNotFound(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_RETRY: u32 = 3;
|
||||||
|
const DATETIME_PATTERN: &str = "%Y-%m-%d %H:%M:%S";
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct CertState {
|
||||||
|
fpr: Fingerprint,
|
||||||
|
processed: bool,
|
||||||
|
updated: bool,
|
||||||
|
// XXX: do we really want to retry within the same update round?
|
||||||
|
retry_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CertState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "{}:{}:{}:{}",
|
||||||
|
self.fpr,
|
||||||
|
self.processed,
|
||||||
|
self.updated,
|
||||||
|
self.retry_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for CertState {
|
||||||
|
type Error = UpdaterError;
|
||||||
|
|
||||||
|
fn try_from(line: &str) -> Result<Self, UpdaterError> {
|
||||||
|
let line = line.trim();
|
||||||
|
let parts: Vec<&str> = line.split(":").collect();
|
||||||
|
|
||||||
|
if parts.len() != 4 {
|
||||||
|
return Err(UpdaterError::WrongRecordSize(parts.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let fpr = parts[0]
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState(parts[0].to_string()))?;
|
||||||
|
let processed = parts[1]
|
||||||
|
.parse::<bool>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState(parts[1].to_string()))?;
|
||||||
|
let updated = parts[2]
|
||||||
|
.parse::<bool>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState(parts[2].to_string()))?;
|
||||||
|
let retry_count = parts[3]
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState(parts[3].to_string()))?;
|
||||||
|
Ok(CertState {
|
||||||
|
fpr,
|
||||||
|
processed,
|
||||||
|
updated,
|
||||||
|
retry_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for CertState {
|
||||||
|
type Error = UpdaterError;
|
||||||
|
|
||||||
|
fn try_from(line: String) -> Result<Self, UpdaterError> {
|
||||||
|
line.as_str().try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertState {
|
||||||
|
|
||||||
|
pub fn new(fpr: Fingerprint) -> Self {
|
||||||
|
Self {
|
||||||
|
fpr,
|
||||||
|
processed: false,
|
||||||
|
updated: false,
|
||||||
|
retry_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this certificate is already processed.
|
||||||
|
pub fn need_processing(&self) -> bool {
|
||||||
|
!(self.processed || (self.retry_count >= MAX_RETRY))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark this certificate as processed - this doesn't mean that
|
||||||
|
/// there was an actual update.
|
||||||
|
pub fn mark_processed(&mut self) {
|
||||||
|
self.processed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark this certificate as updated - this doesn't mark the
|
||||||
|
/// certificate as processed.
|
||||||
|
pub fn mark_updated(&mut self) {
|
||||||
|
self.updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the retry count.
|
||||||
|
pub fn incr_retry_count(&mut self) {
|
||||||
|
self.retry_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct UpdaterState {
|
||||||
|
started: NaiveDateTime,
|
||||||
|
certs: BTreeMap<Fingerprint, CertState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for UpdaterState {
|
||||||
|
type Error = UpdaterError;
|
||||||
|
|
||||||
|
fn try_from(data: String) -> Result<Self, UpdaterError> {
|
||||||
|
let mut result = BTreeMap::new();
|
||||||
|
let mut lines = data.lines();
|
||||||
|
let started = lines
|
||||||
|
.next()
|
||||||
|
.and_then(|l|
|
||||||
|
NaiveDateTime::parse_from_str(l, DATETIME_PATTERN).ok());
|
||||||
|
if started.is_none() {
|
||||||
|
return Err(UpdaterError::DataMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
if line.is_empty() { continue; }
|
||||||
|
let cert_state: CertState = line.try_into()?;
|
||||||
|
result.insert(cert_state.fpr.clone(), cert_state);
|
||||||
|
}
|
||||||
|
Ok(UpdaterState {
|
||||||
|
started: started.unwrap(),
|
||||||
|
certs: result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for UpdaterState {
|
||||||
|
type Error = UpdaterError;
|
||||||
|
|
||||||
|
fn try_from(data: Vec<u8>) -> Result<Self, UpdaterError> {
|
||||||
|
Self::try_from(String::from_utf8_lossy(&data[..]).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for UpdaterState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "{}", self.started.format(DATETIME_PATTERN))?;
|
||||||
|
for cert in self.certs.values() {
|
||||||
|
write!(f, "{}", cert)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UpdaterState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdaterState {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
started: Local::now().naive_local(),
|
||||||
|
certs: BTreeMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next certificate from the updater, which still needs
|
||||||
|
/// processing - this includes retries.
|
||||||
|
pub fn next(&mut self) -> Option<Fingerprint> {
|
||||||
|
let set: Vec<&CertState> = self.certs.values()
|
||||||
|
.filter(|c| c.need_processing())
|
||||||
|
.collect();
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
set.choose(&mut rng)
|
||||||
|
.and_then(|c| Some(c.fpr.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the certificate (identified by its fingerprint) as processed. Also
|
||||||
|
/// mark it as updated if `updated` is true.
|
||||||
|
pub fn mark_processed(&mut self, fpr: &Fingerprint, updated: bool) {
|
||||||
|
if let Some(entry) = self.certs.get_mut(fpr) {
|
||||||
|
entry.mark_processed();
|
||||||
|
if updated {
|
||||||
|
entry.mark_updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the retry count of the certificate (identified by its
|
||||||
|
/// fingerprint).
|
||||||
|
pub fn incr_retry_count(&mut self, fpr: &Fingerprint) {
|
||||||
|
if let Some(entry) = self.certs.get_mut(fpr) {
|
||||||
|
entry.incr_retry_count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan the certificate store for new certificates and add them to the
|
||||||
|
/// state (as unprocessed).
|
||||||
|
pub fn refresh(&mut self, context: &HuskContext<'_>) {
|
||||||
|
|
||||||
|
let count = self.certs.keys().count();
|
||||||
|
|
||||||
|
context.cert_store.certs()
|
||||||
|
.filter(|c| {
|
||||||
|
if let Ok(vc) = c.with_policy(&context.policy, None) {
|
||||||
|
// Ignore revoked certificates.
|
||||||
|
if let RevocationStatus::Revoked(_) = vc.revocation_status() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore non exportable certificates, this includes internal
|
||||||
|
// certificates like the trust root or shadow CAs.
|
||||||
|
vc.cert().exportable()
|
||||||
|
} else {
|
||||||
|
// Certificate is invalid under the current policy.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|c| c.fingerprint())
|
||||||
|
.for_each(|fpr| {
|
||||||
|
if ! self.certs.contains_key(&fpr) {
|
||||||
|
self.certs.insert(fpr.clone(), CertState::new(fpr));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log::debug!("refresh from {} to {}",
|
||||||
|
count,
|
||||||
|
self.certs.keys().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recreate the state from file.
|
||||||
|
pub async fn thaw(file: &Path) -> Result<Self, UpdaterError> {
|
||||||
|
|
||||||
|
if !file.exists() {
|
||||||
|
log::debug!("file not existing: {:?}", file);
|
||||||
|
if let Some(parent) = file.parent() {
|
||||||
|
log::debug!("assert parent directory");
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
}
|
||||||
|
return Ok(Self::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta = file.metadata()
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
let updater_state = if meta.len() == 0 {
|
||||||
|
// no previous state
|
||||||
|
Self::new()
|
||||||
|
} else {
|
||||||
|
let mut handle = File::open(file)
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
let mut content = Vec::with_capacity(meta.len() as usize);
|
||||||
|
handle.read_to_end(&mut content)
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
content.try_into()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(updater_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist state to file.
|
||||||
|
pub async fn freeze(file: &Path, updater_state: &UpdaterState) -> Result<(), UpdaterError> {
|
||||||
|
// freeze comes after thaw, so we assume that the parent directory
|
||||||
|
// exists.
|
||||||
|
let mut options = OpenOptions::new();
|
||||||
|
options.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
options.mode(0o600);
|
||||||
|
|
||||||
|
let mut handle = options.open(file)
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
|
||||||
|
handle.write_all(updater_state.to_string().as_bytes())
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the state file.
|
||||||
|
pub async fn remove(file: &Path) -> Result<(), UpdaterError> {
|
||||||
|
if file.exists() {
|
||||||
|
fs::remove_file(file)
|
||||||
|
.map_err(|e| UpdaterError::IoError(e.to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create some statistics.
|
||||||
|
pub fn summarize(&self) -> (usize, usize) {
|
||||||
|
let mut processed = 0;
|
||||||
|
let mut updated = 0;
|
||||||
|
for cert in self.certs.values() {
|
||||||
|
if cert.processed { processed += 1; }
|
||||||
|
if cert.updated { updated += 1; }
|
||||||
|
}
|
||||||
|
(processed, updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::updater::state::{DATETIME_PATTERN, UpdaterError};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use sequoia_openpgp::Fingerprint;
|
||||||
|
|
||||||
|
use super::{CertState, UpdaterState};
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_deserialize( ) -> Result<(), UpdaterError> {
|
||||||
|
|
||||||
|
let cert_state = CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: false,
|
||||||
|
retry_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: String = format!("{}", cert_state);
|
||||||
|
let deserialized: CertState = serialized.try_into()?;
|
||||||
|
|
||||||
|
assert_eq!(cert_state, deserialized);
|
||||||
|
|
||||||
|
let cert_state = CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: true,
|
||||||
|
retry_count: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: String = format!("{}", cert_state);
|
||||||
|
eprintln!("{}", serialized);
|
||||||
|
|
||||||
|
let deserialized: CertState = serialized.try_into()?;
|
||||||
|
|
||||||
|
assert_eq!(cert_state, deserialized);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_deserialize_store( ) -> Result<(), UpdaterError> {
|
||||||
|
|
||||||
|
let store = UpdaterState {
|
||||||
|
started: NaiveDateTime::parse_from_str("2026-07-11 15:06:10", DATETIME_PATTERN)
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("in test".into()))?,
|
||||||
|
certs: [
|
||||||
|
CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: true,
|
||||||
|
retry_count: 3,
|
||||||
|
},
|
||||||
|
CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB69"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: true,
|
||||||
|
retry_count: 3,
|
||||||
|
},
|
||||||
|
CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB6A"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: true,
|
||||||
|
retry_count: 1,
|
||||||
|
}
|
||||||
|
].into_iter()
|
||||||
|
.map(|c| (c.fpr.clone(), c))
|
||||||
|
.collect::<BTreeMap<Fingerprint, CertState>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized = store.to_string();
|
||||||
|
eprintln!("{}", serialized);
|
||||||
|
let deserialized: UpdaterState = serialized.try_into()?;
|
||||||
|
|
||||||
|
assert_eq!(store, deserialized);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_turnaround() -> Result<(), UpdaterError> {
|
||||||
|
let mut store = UpdaterState {
|
||||||
|
started: NaiveDateTime::parse_from_str("2026-07-11 15:06:10", DATETIME_PATTERN)
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("in test".into()))?,
|
||||||
|
certs: [
|
||||||
|
CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB68"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: false,
|
||||||
|
retry_count: 0,
|
||||||
|
},
|
||||||
|
CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB69"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: false,
|
||||||
|
retry_count: 0,
|
||||||
|
},
|
||||||
|
CertState {
|
||||||
|
fpr: "E936CB3BBCECC7F53769C87C006ECEC2B1DFFB6A"
|
||||||
|
.parse::<Fingerprint>()
|
||||||
|
.map_err(|_e| UpdaterError::BrokenState("Fingerprint wrong in test".into()))?,
|
||||||
|
processed: false,
|
||||||
|
updated: false,
|
||||||
|
retry_count: 0,
|
||||||
|
}
|
||||||
|
].into_iter()
|
||||||
|
.map(|c| (c.fpr.clone(), c))
|
||||||
|
.collect::<BTreeMap<Fingerprint, CertState>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut break_counter = 0;
|
||||||
|
// this loop has to end
|
||||||
|
while let Some(fpr) = store.next() {
|
||||||
|
assert!(break_counter < store.certs.len());
|
||||||
|
break_counter += 1;
|
||||||
|
store.mark_processed(&fpr, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user