mod dns01_strategy;
mod eab;
mod http01_strategy;
mod lifecycle;
mod multi_profile;
mod renewal;
fn test_jobs_config() -> crate::config::JobsConfig {
crate::config::JobsConfig {
poll_interval_ms: 5,
max_attempts: 1,
retry_base_seconds: 0,
retry_max_seconds: 0,
lease_seconds: 5,
retention_days: 0,
..crate::config::JobsConfig::default()
}
}
fn test_queue(database: Arc<Database>) -> crate::jobs::JobQueue {
crate::jobs::JobQueue::new(database, &test_jobs_config())
}
fn test_queue_with(
database: Arc<Database>,
config: &crate::config::JobsConfig,
) -> crate::jobs::JobQueue {
crate::jobs::JobQueue::new(database, config)
}
struct TestRunner {
shutdown: tokio::sync::watch::Sender<bool>,
}
impl TestRunner {
fn start(queue: crate::jobs::JobQueue, signer: &RelaySigner) -> Self {
Self::start_with(queue, signer, test_jobs_config())
}
const DEFAULT_PROFILES: &'static [&'static str] = &["default"];
fn start_notifying(
queue: crate::jobs::JobQueue,
signer: &RelaySigner,
profiles: &[&str],
notifiers: crate::notify::Notifiers,
) -> Self {
Self::start_inner(queue, signer, profiles, test_jobs_config(), Some(notifiers))
}
fn start_with(
queue: crate::jobs::JobQueue,
signer: &RelaySigner,
config: crate::config::JobsConfig,
) -> Self {
Self::start_inner(queue, signer, Self::DEFAULT_PROFILES, config, None)
}
fn start_inner(
queue: crate::jobs::JobQueue,
signer: &RelaySigner,
profiles: &[&str],
config: crate::config::JobsConfig,
notifiers: Option<crate::notify::Notifiers>,
) -> Self {
let mut registry = crate::jobs::JobRegistry::new();
registry
.register(Arc::new(relay_handler(signer, profiles)))
.unwrap();
if let Some(notifiers) = notifiers {
registry
.register(Arc::new(crate::notify::NotifyJob::new(notifiers)))
.unwrap();
}
let (shutdown, receiver) = tokio::sync::watch::channel(false);
crate::jobs::spawn_runner(queue, Arc::new(registry), &config, receiver);
Self { shutdown }
}
}
impl Drop for TestRunner {
fn drop(&mut self) {
let _ = self.shutdown.send(true);
}
}
fn test_resolver() -> Arc<dyn crate::dns::Resolver> {
Arc::new(crate::dns::HickoryResolver::from_system_uncached().unwrap())
}
pub(super) fn order_id(name: &str) -> String {
let mut bytes = [0u8; 16];
let name = name.as_bytes();
let take = name.len().min(16);
bytes[..take].copy_from_slice(&name[..take]);
uuid::Uuid::from_bytes(bytes).to_string()
}
use super::account::kid_path;
use super::client::UpstreamError;
use super::flow::settle;
use super::http01::TokenStore;
use super::*;
use crate::audit::ClientContext;
use crate::notify::{NotifyDispatcher, NotifyEvent};
use crate::signer::local_ca::LocalCa;
use crate::sqlite::account::Account;
use crate::sqlite::nonce::now_secs;
use crate::sqlite::order::Order;
use crate::sqlite::status::OrderStatus;
use crate::testutil::TempDir;
use std::collections::HashMap;
use std::path::PathBuf;
use testsrv::{Script, Upstream};
fn key_path(dir: &TempDir) -> String {
dir.join("upstream.key").to_string_lossy().into_owned()
}
fn config(upstream: &Upstream, dir: &TempDir) -> RelayConfig {
RelayConfig {
directory_url: upstream.directory_url(),
account_key_path: key_path(dir),
poll_interval_ms: 5,
poll_timeout_secs: 5,
..RelayConfig::default()
}
}
async fn database() -> Arc<Database> {
Arc::new(Database::connect_in_memory().await.unwrap())
}
fn no_notifiers() -> crate::notify::Notifiers {
HashMap::new().into()
}
fn relay_parts(
database: Arc<Database>,
notifiers: crate::notify::Notifiers,
jobs: crate::jobs::JobQueue,
) -> crate::signer::SignerParts {
crate::signer::SignerParts {
database: database.clone(),
notifiers,
metrics: crate::testutil::test_metrics(database),
egress: crate::testutil::egress_with(test_resolver()),
jobs,
}
}
#[derive(Default)]
struct StubTokens {
published: std::sync::Mutex<Vec<(String, String)>>,
retracted: std::sync::Mutex<Vec<String>>,
live: std::sync::Mutex<HashMap<String, String>>,
}
impl StubTokens {
fn published(&self) -> Vec<(String, String)> {
self.published.lock().unwrap().clone()
}
}
impl http01::TokenStore for StubTokens {
fn publish(&self, token: &str, key_authorization: &str) {
self.published
.lock()
.unwrap()
.push((token.to_string(), key_authorization.to_string()));
self.live
.lock()
.unwrap()
.insert(token.to_string(), key_authorization.to_string());
}
fn retract(&self, token: &str) {
self.retracted.lock().unwrap().push(token.to_string());
self.live.lock().unwrap().remove(token);
}
fn lookup(&self, token: &str) -> Option<String> {
self.live.lock().unwrap().get(token).cloned()
}
}
fn with_tokens(signer: RelaySigner, tokens: Arc<StubTokens>) -> RelaySigner {
let inner = Arc::try_unwrap(signer.0).unwrap_or_else(|_| panic!("sole owner"));
RelaySigner(Arc::new(Inner {
strategy: ChallengeStrategy::Http01(tokens),
..inner
}))
}
fn relay_handler(signer: &RelaySigner, profiles: &[&str]) -> flow::RelayJob {
flow::RelayJob::new(
signer.0.database.clone(),
profiles
.iter()
.map(|profile| {
(
(*profile).to_string(),
signer
.relay_state()
.expect("a relay backend always has state to hand over"),
)
})
.collect(),
)
}
struct RecordingNotifyBackend {
events: std::sync::Mutex<Vec<NotifyEvent>>,
}
impl RecordingNotifyBackend {
fn new() -> Self {
Self {
events: std::sync::Mutex::new(Vec::new()),
}
}
}
#[async_trait]
impl crate::notify::NotifyBackend for RecordingNotifyBackend {
fn name(&self) -> &'static str {
"recording"
}
async fn send(&self, event: &NotifyEvent) -> Result<(), crate::notify::NotifyError> {
self.events.lock().unwrap().push(event.clone());
Ok(())
}
}
async fn await_recorded(recorder: &Arc<RecordingNotifyBackend>) {
for _ in 0..200 {
if !recorder.events.lock().unwrap().is_empty() {
return;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
panic!("no notification was delivered within the budget");
}
fn recording_slot(recorder: Arc<RecordingNotifyBackend>) -> crate::notify::BackendSlot {
let every: Vec<String> = crate::config::ALL_NOTIFY_EVENTS
.iter()
.map(|kind| (*kind).to_string())
.collect();
crate::notify::BackendSlot::new("recording", recorder, &every)
}
async fn ready_order_for(profile: &str, database: Arc<Database>) -> Order {
let (account, _) = Account::find_or_create(
profile,
&crate::random::random_bytes::<16>(),
Vec::new(),
&ClientContext::default(),
&database,
)
.await
.unwrap();
let mut order = Order::create(
profile,
account.id,
vec![Identifier::dns("example.com")],
now_secs() + 3600,
None,
None,
&database,
)
.await
.unwrap();
order.mark_ready(&database).await.unwrap();
order
}
async fn real_chain() -> String {
let ca = LocalCa::generate_in_memory("ecdsa-p256", 90).unwrap();
let key_pair = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
let csr = params.serialize_request(&key_pair).unwrap();
match ca
.issue(
"ord-x",
csr.der(),
&identifiers(),
RequestedValidity::default(),
)
.await
{
Ok(IssueOutcome::Issued(chain)) => chain,
_ => panic!("the in-memory CA must issue"),
}
}
async fn ready_order(database: Arc<Database>) -> Order {
let (account, _) = Account::find_or_create(
"default",
&crate::random::random_bytes::<16>(),
Vec::new(),
&ClientContext::default(),
&database,
)
.await
.unwrap();
let mut order = Order::create(
"default",
account.id,
vec![Identifier::dns("example.com")],
now_secs() + 3600,
None,
None,
&database,
)
.await
.unwrap();
order.mark_ready(&database).await.unwrap();
order
}
fn ca_signed_leaf_with_aki() -> Vec<u8> {
let ca_key = rcgen::KeyPair::generate().unwrap();
let mut ca_params = rcgen::CertificateParams::new(vec!["ca.example".to_string()]).unwrap();
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
let ca_pem = ca_params.self_signed(&ca_key).unwrap().pem();
let issuer = rcgen::Issuer::from_ca_cert_pem(&ca_pem, ca_key).unwrap();
let leaf_key = rcgen::KeyPair::generate().unwrap();
let mut leaf_params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
leaf_params.use_authority_key_identifier_extension = true;
leaf_params
.signed_by(&leaf_key, &issuer)
.unwrap()
.der()
.to_vec()
}
fn csr_der() -> Vec<u8> {
let key_pair = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
params.serialize_request(&key_pair).unwrap().der().to_vec()
}
fn startup_error(result: anyhow::Result<RelaySigner>) -> String {
match result {
Err(error) => error.to_string(),
Ok(_) => panic!("this configuration must not build"),
}
}
fn identifiers() -> Vec<Identifier> {
vec![Identifier::dns("example.com")]
}
async fn await_status(database: Arc<Database>, order_id: &str, wanted: OrderStatus) -> Order {
for _ in 0..200 {
let order = Order::find_by_id(order_id, &database)
.await
.unwrap()
.unwrap();
if order.status == wanted {
return order;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
let order = Order::find_by_id(order_id, &database)
.await
.unwrap()
.unwrap();
panic!("order stayed {}, expected {wanted}", order.status);
}