diff --git a/src/config.rs b/src/config.rs index d62ea8a..e830d28 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,7 +29,7 @@ const ALLOWED_FILE_EXT: &[&str] = &["toml"]; pub struct AcmedConfig { pub(in crate::config) global: Option, #[serde(default)] - pub(in crate::config) endpoint: HashMap, + pub endpoint: HashMap, #[serde(default, rename = "rate-limit")] pub(in crate::config) rate_limit: HashMap, #[serde(default)] @@ -42,6 +42,12 @@ pub struct AcmedConfig { pub(in crate::config) certificate: Vec, } +impl AcmedConfig { + pub fn get_global_root_certs(&self) -> Option<&[PathBuf]> { + self.global.as_ref().map(|g| g.root_certificates.as_slice()) + } +} + impl<'de> Deserialize<'de> for AcmedConfig { fn deserialize(deserializer: D) -> Result where diff --git a/src/config/endpoint.rs b/src/config/endpoint.rs index 387bb21..98e580f 100644 --- a/src/config/endpoint.rs +++ b/src/config/endpoint.rs @@ -11,7 +11,7 @@ pub struct Endpoint { pub(in crate::config) rate_limits: Vec, pub(in crate::config) renew_delay: Option, #[serde(default)] - pub(in crate::config) root_certificates: Vec, + pub root_certificates: Vec, #[serde(default)] pub(in crate::config) tos_agreed: bool, pub(in crate::config) url: String, diff --git a/src/http.rs b/src/http.rs index 3d82f4f..9934f43 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,12 +1,17 @@ use crate::config::AcmedConfig; -use anyhow::{Context, Result}; -use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::{header, Client, ClientBuilder, Request, Response}; +use anyhow::Result; +use reqwest::header::HeaderMap; +use reqwest::{header, Certificate, Client, ClientBuilder, Request, Response}; +use std::collections::HashMap; +use std::fs::File; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; use tokio::sync::{mpsc, oneshot}; #[derive(Debug)] struct RawRequest { request: Request, + endpoint: String, tx: oneshot::Sender>, } @@ -16,9 +21,13 @@ pub struct HttpClient { } impl HttpClient { - pub async fn send(&self, request: Request) -> Result { + pub async fn send>(&self, endpoint: S, request: Request) -> Result { let (tx, rx) = oneshot::channel(); - let raw_req = RawRequest { request, tx }; + let raw_req = RawRequest { + request, + endpoint: endpoint.as_ref().to_string(), + tx, + }; self.tx.send(raw_req)?; let ret = rx.await?; if let Err(ref e) = ret { @@ -28,16 +37,29 @@ impl HttpClient { } } +#[derive(Debug)] +struct HttpEndpoint { + client: Client, +} + #[derive(Debug)] pub(super) struct HttpRoutine { tx: mpsc::UnboundedSender, rx: mpsc::UnboundedReceiver, + endpoints: HashMap, } impl HttpRoutine { pub(super) fn new(config: &AcmedConfig) -> Self { - let (tx, mut rx) = mpsc::unbounded_channel(); - Self { tx, rx } + let (tx, rx) = mpsc::unbounded_channel(); + let mut endpoints = HashMap::with_capacity(config.endpoint.len()); + for (name, edp) in &config.endpoint { + tracing::debug!("loading endpoint: {name}"); + let client = get_http_client(config.get_global_root_certs(), &edp.root_certificates); + let endpoint = HttpEndpoint { client }; + endpoints.insert(name.to_owned(), endpoint); + } + Self { tx, rx, endpoints } } pub(super) fn get_client(&self) -> HttpClient { @@ -48,28 +70,62 @@ impl HttpRoutine { pub(super) async fn run(mut self) { tracing::trace!("starting the http routine"); - let client = self.get_http_client(); while let Some(raw_req) = self.rx.recv().await { tracing::debug!("new http request: {:?}", raw_req.request); - let ret = client.execute(raw_req.request).await; - let _ = raw_req.tx.send(ret); + match self.endpoints.get(&raw_req.endpoint) { + Some(edp) => { + let ret = edp.client.execute(raw_req.request).await; + let _ = raw_req.tx.send(ret); + } + None => { + tracing::error!("{}: endpoint not found", raw_req.endpoint); + } + } } tracing::warn!("the http routine has stopped"); } +} - fn get_http_client(&self) -> Client { - let useragent = format!( - "{}/{} ({}) {}", - env!("CARGO_BIN_NAME"), - env!("CARGO_PKG_VERSION"), - env!("ACMED_TARGET"), - env!("ACMED_HTTP_LIB_AGENT") - ); - let mut client_builder = ClientBuilder::new(); - let mut default_headers = HeaderMap::new(); - default_headers.append(header::ACCEPT_LANGUAGE, "en-US,en;q=0.5".parse().unwrap()); - default_headers.append(header::USER_AGENT, useragent.parse().unwrap()); - client_builder = client_builder.default_headers(default_headers); - client_builder.build().unwrap() +macro_rules! add_root_cert { + ($builder: ident, $iter: ident) => { + for cert_path in $iter { + match get_cert_pem(cert_path) { + Ok(cert) => { + $builder = $builder.add_root_certificate(cert); + tracing::debug!("root certificate loaded: {}", cert_path.display()); + } + Err(e) => tracing::error!( + "{} unable to load root certificate: {e:#?}", + cert_path.display() + ), + } + } + }; +} + +fn get_http_client(base_certs_opt: Option<&[PathBuf]>, end_certs: &[PathBuf]) -> Client { + let useragent = format!( + "{}/{} ({}) {}", + env!("CARGO_BIN_NAME"), + env!("CARGO_PKG_VERSION"), + env!("ACMED_TARGET"), + env!("ACMED_HTTP_LIB_AGENT") + ); + let mut client_builder = ClientBuilder::new(); + let mut default_headers = HeaderMap::new(); + default_headers.append(header::ACCEPT_LANGUAGE, "en-US,en;q=0.5".parse().unwrap()); + default_headers.append(header::USER_AGENT, useragent.parse().unwrap()); + client_builder = client_builder.default_headers(default_headers); + if let Some(base_certs) = base_certs_opt { + add_root_cert!(client_builder, base_certs); } + add_root_cert!(client_builder, end_certs); + client_builder.build().unwrap() +} + +fn get_cert_pem(cert_path: &Path) -> Result { + let mut buff = Vec::new(); + File::open(cert_path)?.read_to_end(&mut buff)?; + let crt = reqwest::Certificate::from_pem(&buff)?; + Ok(crt) } diff --git a/src/main.rs b/src/main.rs index 3f51e3b..8c1d00e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod http; mod log; use crate::config::AcmedConfig; -use crate::http::{HttpClient, HttpRoutine}; +use crate::http::HttpRoutine; use anyhow::{Context, Result}; use clap::Parser; use daemonize::Daemonize; @@ -60,17 +60,20 @@ async fn start(cnf: AcmedConfig) { // TODO: REMOVE ME use reqwest::{Method, Request, Url}; let rsp = http_client - .send(Request::new( - Method::GET, - Url::parse("https://example.org").unwrap(), - )) + .send( + "my-ca", + Request::new(Method::GET, Url::parse("https://example.org").unwrap()), + ) .await; tracing::debug!("{rsp:?}"); let rsp = http_client - .send(Request::new( - Method::GET, - Url::parse("https://example.com/directory/").unwrap(), - )) + .send( + "my-ca", + Request::new( + Method::GET, + Url::parse("https://example.com/directory/").unwrap(), + ), + ) .await; tracing::debug!("{rsp:?}"); } diff --git a/tests/config/simple/simple.toml b/tests/config/simple/simple.toml index b6f7663..e907fed 100644 --- a/tests/config/simple/simple.toml +++ b/tests/config/simple/simple.toml @@ -1,6 +1,7 @@ [global] accounts_directory = "/tmp/example/account/dir" certificates_directory = "/tmp/example/cert/dir/" +root_certificates = ["tests/root_certs/igc-a 2011.pem"] [account."toto"] contacts = [ @@ -15,6 +16,7 @@ period = "1s" [endpoint."my-ca"] url = "https://acme-v02.ac1.example.org/directory" rate_limits = ["my-ca-limit"] +root_certificates = ["tests/root_certs/igc-a 2018.pem"] tos_agreed = true [hook."hook-1"] diff --git a/tests/root_certs/igc-a 2011.pem b/tests/root_certs/igc-a 2011.pem new file mode 100644 index 0000000..bc88769 --- /dev/null +++ b/tests/root_certs/igc-a 2011.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/tests/root_certs/igc-a 2018.pem b/tests/root_certs/igc-a 2018.pem new file mode 100644 index 0000000..94e5cbd --- /dev/null +++ b/tests/root_certs/igc-a 2018.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE-----