acme_proxy_protocol/handlers/
authz.rs1use axum::{
12 Extension, Json,
13 extract::{Path, State},
14 http::{HeaderValue, StatusCode, header},
15 response::{IntoResponse, Response},
16};
17use serde::Deserialize;
18use serde_json::Value;
19use std::net::IpAddr;
20use tracing::{error, info, instrument, warn};
21
22use crate::acme::access::{load_owned_authz, load_owned_challenge, signer_account};
23use crate::acme::order::{OrderService, challenge_can_be_triggered};
24use crate::extractors::acme::AcmeOptionalPayload;
25use crate::router::AppState;
26use acme_proxy_core::client::ClientIp;
27use acme_proxy_core::error::Problem;
28use acme_proxy_jobs::jobs::JobQueue;
29use acme_proxy_store::authz::Authorization;
30use acme_proxy_store::authz::Challenge;
31use acme_proxy_store::authz::ValidationClaim;
32use acme_proxy_store::nonce::now_secs;
33use acme_proxy_store::order::Order;
34use acme_proxy_store::status::ChallengeStatus;
35
36#[derive(Debug, Deserialize)]
39pub struct AuthzUpdatePayload {
40 pub status: Option<String>,
41}
42
43#[instrument(name = "post_authz", skip_all, fields(authz_id = %id))]
49pub async fn post_authz(
50 State(state): State<AppState>,
51 Path(id): Path<String>,
52 AcmeOptionalPayload {
53 payload,
54 pubkey,
55 account,
56 ..
57 }: AcmeOptionalPayload<AuthzUpdatePayload>,
58) -> Result<Response, Problem> {
59 info!(
60 event = "authz_lookup_requested",
61 outcome = "progress",
62 authz_id = %id,
63 deactivating = payload.is_some(),
64 );
65 let AppState {
66 database,
67 profile,
68 audit,
69 ..
70 } = state;
71 let base = &profile.base_url;
72
73 let account = signer_account(account, &profile.name, &pubkey, &database).await?;
77 let (mut authz, mut order) = load_owned_authz(&id, &account, &database).await?;
78
79 if let Some(update) = payload {
80 if update.status.as_deref() != Some("deactivated") {
83 warn!(event = "authz_update_unsupported", outcome = "failure", authz_id = %id, status = ?update.status);
84 return Err(Problem::malformed(
85 "Only {\"status\": \"deactivated\"} is supported on an authorization",
86 ));
87 }
88 let orders = OrderService {
89 database: &database,
90 audit: &audit,
91 profile: &profile,
92 };
93 orders.deactivate_authz(&mut authz, &mut order).await?;
94 }
95
96 let challenges = Challenge::find_by_authz(authz.id, &database)
97 .await
98 .map_err(|error| {
99 error!(
100 event = "challenge_list_failed",
101 outcome = "failure",
102 authz_id = %id,
103 error = %error
104 );
105 Problem::server_internal("Challenge lookup failed")
106 })?;
107
108 let mut response = Json(authz.to_json(base, &challenges)).into_response();
109 add_pending_retry_after(&mut response, authz.status.as_str());
110 Ok(response)
111}
112
113fn add_pending_retry_after(response: &mut Response, status: &str) {
126 if status == "pending" || status == "processing" {
127 response.headers_mut().insert(
128 header::RETRY_AFTER,
129 HeaderValue::from_static(super::POLL_RETRY_AFTER),
130 );
131 }
132}
133
134#[instrument(name = "post_challenge", skip_all, fields(challenge_id = %id))]
141pub async fn post_challenge(
142 State(state): State<AppState>,
143 Path(id): Path<String>,
144 Extension(ClientIp(client_ip)): Extension<ClientIp>,
145 AcmeOptionalPayload {
146 payload,
147 pubkey,
148 account,
149 ..
150 }: AcmeOptionalPayload<Value>,
151) -> Result<Response, Problem> {
152 info!(
153 event = "challenge_trigger_requested",
154 outcome = "progress",
155 challenge_id = %id,
156 triggering = payload.is_some(),
157 );
158 let AppState {
159 database,
160 profile,
161 audit,
162 jobs,
163 ..
164 } = state;
165 let base = &profile.base_url;
166 let orders = OrderService {
167 database: &database,
168 audit: &audit,
169 profile: &profile,
170 };
171
172 let account = signer_account(account, &profile.name, &pubkey, &database).await?;
173 let (mut challenge, authz, order) = load_owned_challenge(&id, &account, &database).await?;
174
175 if payload.is_some()
177 && let Some(early) = trigger_validation(
178 &orders,
179 &jobs,
180 &mut challenge,
181 &authz,
182 &order,
183 &id,
184 client_ip,
185 )
186 .await?
187 {
188 return Ok(early);
189 }
190
191 info!(
192 event = "challenge_answered",
193 outcome = "success",
194 challenge_id = %id,
195 authz_id = %authz.id,
196 order_id = %order.id,
197 status = %challenge.status
198 );
199 let up_link = format!("<{base}/authz/{}>;rel=\"up\"", authz.id);
200 let mut response = (
201 StatusCode::OK,
202 [(header::LINK, up_link)],
203 Json(challenge.to_json(base)),
204 )
205 .into_response();
206 if challenge.status != ChallengeStatus::Pending
211 || challenge_can_be_triggered(&authz, &order, now_secs())
212 {
213 add_pending_retry_after(&mut response, challenge.status.as_str());
214 }
215 Ok(response)
216}
217
218async fn trigger_validation(
224 orders: &OrderService<'_>,
225 jobs: &JobQueue,
226 challenge: &mut Challenge,
227 authz: &Authorization,
228 order: &Order,
229 id: &str,
230 client_ip: Option<IpAddr>,
231) -> Result<Option<Response>, Problem> {
232 let claim = orders.claim_challenge(challenge, authz, order).await?;
239 if claim == ValidationClaim::Limited {
240 let mut response = Problem::rate_limited(
244 "Too many validations are already running for this account; retry shortly",
245 )
246 .into_response();
247 response.headers_mut().insert(
248 header::RETRY_AFTER,
249 HeaderValue::from_static(super::POLL_RETRY_AFTER),
250 );
251 return Ok(Some(response));
252 }
253 if claim == ValidationClaim::Claimed {
254 let queued = jobs
255 .enqueue(crate::acme::validate::challenge_validate_spec(
256 id,
257 client_ip,
258 authz.expires,
259 ))
260 .await;
261
262 if let Err(error) = queued {
268 error!(
269 event = "challenge_validation_enqueue_failed",
270 outcome = "failure",
271 challenge_id = %id,
272 error = %error
273 );
274 let _ = challenge.release_validation_claim(orders.database).await;
275 return Err(Problem::server_internal(
276 "Challenge validation could not be queued",
277 ));
278 }
279 }
280 Ok(None)
281}