use axum::{
Extension, Json,
extract::{Path, State},
http::{HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use serde_json::Value;
use std::net::IpAddr;
use tracing::{error, info, instrument, warn};
use crate::acme::access::{load_owned_authz, load_owned_challenge, signer_account};
use crate::acme::order::{OrderService, challenge_can_be_triggered};
use crate::extractors::acme::AcmeOptionalPayload;
use crate::router::AppState;
use acme_proxy_core::client::ClientIp;
use acme_proxy_core::error::Problem;
use acme_proxy_jobs::jobs::JobQueue;
use acme_proxy_store::authz::Authorization;
use acme_proxy_store::authz::Challenge;
use acme_proxy_store::authz::ValidationClaim;
use acme_proxy_store::nonce::now_secs;
use acme_proxy_store::order::Order;
use acme_proxy_store::status::ChallengeStatus;
#[derive(Debug, Deserialize)]
pub struct AuthzUpdatePayload {
pub status: Option<String>,
}
#[instrument(name = "post_authz", skip_all, fields(authz_id = %id))]
pub async fn post_authz(
State(state): State<AppState>,
Path(id): Path<String>,
AcmeOptionalPayload {
payload,
pubkey,
account,
..
}: AcmeOptionalPayload<AuthzUpdatePayload>,
) -> Result<Response, Problem> {
info!(
event = "authz_lookup_requested",
outcome = "progress",
authz_id = %id,
deactivating = payload.is_some(),
);
let AppState {
database,
profile,
audit,
..
} = state;
let base = &profile.base_url;
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
let (mut authz, mut order) = load_owned_authz(&id, &account, &database).await?;
if let Some(update) = payload {
if update.status.as_deref() != Some("deactivated") {
warn!(event = "authz_update_unsupported", outcome = "failure", authz_id = %id, status = ?update.status);
return Err(Problem::malformed(
"Only {\"status\": \"deactivated\"} is supported on an authorization",
));
}
let orders = OrderService {
database: &database,
audit: &audit,
profile: &profile,
};
orders.deactivate_authz(&mut authz, &mut order).await?;
}
let challenges = Challenge::find_by_authz(authz.id, &database)
.await
.map_err(|error| {
error!(
event = "challenge_list_failed",
outcome = "failure",
authz_id = %id,
error = %error
);
Problem::server_internal("Challenge lookup failed")
})?;
let mut response = Json(authz.to_json(base, &challenges)).into_response();
add_pending_retry_after(&mut response, authz.status.as_str());
Ok(response)
}
fn add_pending_retry_after(response: &mut Response, status: &str) {
if status == "pending" || status == "processing" {
response.headers_mut().insert(
header::RETRY_AFTER,
HeaderValue::from_static(super::POLL_RETRY_AFTER),
);
}
}
#[instrument(name = "post_challenge", skip_all, fields(challenge_id = %id))]
pub async fn post_challenge(
State(state): State<AppState>,
Path(id): Path<String>,
Extension(ClientIp(client_ip)): Extension<ClientIp>,
AcmeOptionalPayload {
payload,
pubkey,
account,
..
}: AcmeOptionalPayload<Value>,
) -> Result<Response, Problem> {
info!(
event = "challenge_trigger_requested",
outcome = "progress",
challenge_id = %id,
triggering = payload.is_some(),
);
let AppState {
database,
profile,
audit,
jobs,
..
} = state;
let base = &profile.base_url;
let orders = OrderService {
database: &database,
audit: &audit,
profile: &profile,
};
let account = signer_account(account, &profile.name, &pubkey, &database).await?;
let (mut challenge, authz, order) = load_owned_challenge(&id, &account, &database).await?;
if payload.is_some()
&& let Some(early) = trigger_validation(
&orders,
&jobs,
&mut challenge,
&authz,
&order,
&id,
client_ip,
)
.await?
{
return Ok(early);
}
info!(
event = "challenge_answered",
outcome = "success",
challenge_id = %id,
authz_id = %authz.id,
order_id = %order.id,
status = %challenge.status
);
let up_link = format!("<{base}/authz/{}>;rel=\"up\"", authz.id);
let mut response = (
StatusCode::OK,
[(header::LINK, up_link)],
Json(challenge.to_json(base)),
)
.into_response();
if challenge.status != ChallengeStatus::Pending
|| challenge_can_be_triggered(&authz, &order, now_secs())
{
add_pending_retry_after(&mut response, challenge.status.as_str());
}
Ok(response)
}
async fn trigger_validation(
orders: &OrderService<'_>,
jobs: &JobQueue,
challenge: &mut Challenge,
authz: &Authorization,
order: &Order,
id: &str,
client_ip: Option<IpAddr>,
) -> Result<Option<Response>, Problem> {
let claim = orders.claim_challenge(challenge, authz, order).await?;
if claim == ValidationClaim::Limited {
let mut response = Problem::rate_limited(
"Too many validations are already running for this account; retry shortly",
)
.into_response();
response.headers_mut().insert(
header::RETRY_AFTER,
HeaderValue::from_static(super::POLL_RETRY_AFTER),
);
return Ok(Some(response));
}
if claim == ValidationClaim::Claimed {
let queued = jobs
.enqueue(crate::acme::validate::challenge_validate_spec(
id,
client_ip,
authz.expires,
))
.await;
if let Err(error) = queued {
error!(
event = "challenge_validation_enqueue_failed",
outcome = "failure",
challenge_id = %id,
error = %error
);
let _ = challenge.release_validation_claim(orders.database).await;
return Err(Problem::server_internal(
"Challenge validation could not be queued",
));
}
}
Ok(None)
}