use crate::{
solver::{boxed_err, Solver},
Account, Directory,
};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use reqwest::Client;
use serde::Serialize;
use std::{collections::HashMap, error::Error, sync::Arc};
pub const TEST_URL: &str = "https://10.30.50.2:14000/dir";
pub fn client() -> Client {
Client::builder()
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
.user_agent("lers/testing")
.build()
.unwrap()
}
pub async fn directory() -> Directory {
Directory::builder(TEST_URL)
.client(client())
.build()
.await
.unwrap()
}
pub async fn directory_with_http01_solver() -> Directory {
Directory::builder(TEST_URL)
.client(client())
.http01_solver(Box::new(EXTERNAL_HTTP01_SOLVER.clone()))
.build()
.await
.unwrap()
}
pub async fn directory_with_dns01_solver() -> Directory {
Directory::builder(TEST_URL)
.client(client())
.dns01_solver(Box::new(EXTERNAL_DNS01_SOLVER.clone()))
.build()
.await
.unwrap()
}
pub async fn account(directory: Directory) -> Account {
directory
.account()
.contacts(vec!["mailto:test@user.com".into()])
.terms_of_service_agreed(true)
.create_if_not_exists()
.await
.unwrap()
}
static EXTERNAL_HTTP01_SOLVER: Lazy<ExternalHttp01Solver> = Lazy::new(|| ExternalHttp01Solver {
domains: Arc::default(),
client: Client::new(),
});
static EXTERNAL_DNS01_SOLVER: Lazy<ExternalDns01Solver> = Lazy::new(|| ExternalDns01Solver {
domains: Arc::default(),
client: Client::new(),
});
const ADD_A_RECORD_URL: &str = "http://10.30.50.3:8055/add-a";
const CLEAR_A_RECORD_URL: &str = "http://10.30.50.3:8055/clear-a";
const ADD_HTTP_01_URL: &str = "http://10.30.50.3:8055/add-http01";
const DELETE_HTTP_01_URL: &str = "http://10.30.50.3:8055/del-http-01";
const ADD_DNS_01_URL: &str = "http://10.30.50.3:8055/set-txt";
const CLEAR_DNS_01_URL: &str = "http://10.30.50.3:8055/clear-txt";
#[derive(Debug, Clone)]
struct ExternalHttp01Solver {
domains: Arc<Mutex<HashMap<String, String>>>,
client: Client,
}
#[async_trait::async_trait]
impl Solver for ExternalHttp01Solver {
async fn present(
&self,
domain: String,
token: String,
key_authorization: String,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
request(
&self.client,
ADD_A_RECORD_URL,
DnsRequest {
host: &domain,
addresses: Some(&["10.30.50.3"]),
},
true,
)
.await?;
request(
&self.client,
ADD_HTTP_01_URL,
Http01Request {
token: &token,
content: Some(&key_authorization),
},
true,
)
.await?;
{
let mut domains = self.domains.lock();
domains.insert(token, domain);
}
Ok(())
}
async fn cleanup(&self, token: &str) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
let domain = {
let mut domains = self.domains.lock();
domains.remove(token)
};
let Some(domain) = domain else { panic!("domain for token {token:?} does not exist") };
request(
&self.client,
CLEAR_A_RECORD_URL,
DnsRequest {
host: &domain,
addresses: None,
},
false,
)
.await?;
request(
&self.client,
DELETE_HTTP_01_URL,
Http01Request {
token,
content: None,
},
false,
)
.await?;
Ok(())
}
}
#[derive(Debug, Clone)]
struct ExternalDns01Solver {
domains: Arc<Mutex<HashMap<String, String>>>,
client: Client,
}
#[async_trait::async_trait]
impl Solver for ExternalDns01Solver {
async fn present(
&self,
domain: String,
token: String,
key_authorization: String,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
request(
&self.client,
ADD_DNS_01_URL,
Dns01Request {
host: format!("_acme-challenge.{domain}."),
value: Some(&key_authorization),
},
true,
)
.await?;
{
let mut domains = self.domains.lock();
domains.insert(token, domain);
}
Ok(())
}
async fn cleanup(&self, token: &str) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
let domain = {
let mut domains = self.domains.lock();
domains.remove(token)
};
let Some(domain) = domain else { panic!("domain for token {token:?} does not exist") };
request(
&self.client,
CLEAR_DNS_01_URL,
Dns01Request {
host: format!("_acme-challenge.{domain}."),
value: None,
},
false,
)
.await?;
Ok(())
}
}
#[derive(Debug, Serialize)]
struct DnsRequest<'s> {
host: &'s str,
#[serde(skip_serializing_if = "Option::is_none")]
addresses: Option<&'s [&'s str]>,
}
#[derive(Debug, Serialize)]
struct Http01Request<'s> {
token: &'s str,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<&'s str>,
}
#[derive(Debug, Serialize)]
struct Dns01Request<'s> {
host: String,
value: Option<&'s str>,
}
async fn request<S>(
client: &Client,
url: &str,
body: S,
raise_for_status: bool,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
where
S: Serialize,
{
let response = client
.post(url)
.json(&body)
.send()
.await
.map_err(boxed_err)?;
if raise_for_status {
response.error_for_status().map_err(boxed_err)?;
}
Ok(())
}