use axum::{
Extension, Json,
extract::{Path, State},
http::{HeaderValue, StatusCode, header},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use serde_json::Value;
use tracing::{error, info, instrument, warn};
use crate::acme::access::{load_owned_authz, load_owned_challenge, signer_account};
use crate::acme::order::OrderService;
use crate::extractors::acme::{AcmeOptionalPayload, AcmeRequest};
use crate::router::AppState;
use acme_proxy_core::client::ClientIp;
use acme_proxy_core::error::Problem;
use acme_proxy_store::authz::Challenge;
use acme_proxy_store::authz::ValidationClaim;
#[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>,
AcmeRequest {
pubkey, account, ..
}: AcmeRequest<Value>,
) -> Result<Response, Problem> {
info!(
event = "challenge_trigger_requested",
outcome = "progress",
challenge_id = %id
);
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?;
let claim = orders
.claim_challenge(&mut 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(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(&database).await;
return Err(Problem::server_internal(
"Challenge validation could not be queued",
));
}
}
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();
add_pending_retry_after(&mut response, challenge.status.as_str());
Ok(response)
}