Skip to content

Commit 003a368

Browse files
committed
ACME: set a limit on server-indicated retry intervals.
Previously, it was possible to suspend the module execution by sending a sufficiently high Retry-After value. Now we will refuse to wait longer than one minute.
1 parent a24dcae commit 003a368

File tree

3 files changed

+50
-8
lines changed

3 files changed

+50
-8
lines changed

src/acme.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ pub mod solvers;
3434
pub mod types;
3535

3636
const DEFAULT_RETRY_INTERVAL: Duration = Duration::from_secs(1);
37-
const MAX_RETRY_INTERVAL: Duration = Duration::from_secs(8);
37+
38+
/// Upper limit for locally generated increasing backoff interval.
39+
const MAX_BACKOFF_INTERVAL: Duration = Duration::from_secs(8);
40+
41+
/// Upper limit for server-generated retry intervals (Retry-After).
42+
const MAX_SERVER_RETRY_INTERVAL: Duration = Duration::from_secs(60);
43+
3844
static REPLAY_NONCE: http::HeaderName = http::HeaderName::from_static("replay-nonce");
3945

4046
pub enum NewAccountOutput<'a> {
@@ -232,10 +238,22 @@ where
232238

233239
let err: types::Problem = deserialize_body(res.body())?;
234240

235-
let retriable = matches!(
236-
err.kind,
237-
types::ErrorKind::BadNonce | types::ErrorKind::RateLimited
238-
);
241+
let retriable = match err.kind {
242+
types::ErrorKind::RateLimited => {
243+
// The server may ask us to retry in several hours or days.
244+
if let Some(val) = res
245+
.headers()
246+
.get(http::header::RETRY_AFTER)
247+
.and_then(parse_retry_after)
248+
.filter(|x| x > &MAX_SERVER_RETRY_INTERVAL)
249+
{
250+
return Err(RequestError::RateLimited(val));
251+
}
252+
true
253+
}
254+
types::ErrorKind::BadNonce => true,
255+
_ => false,
256+
};
239257

240258
if retriable && wait_for_retry(&res, &mut tries).await {
241259
ngx_log_debug!(self.log.as_ptr(), "retrying failed request ({err})");
@@ -416,7 +434,7 @@ where
416434
_ => order.status = OrderStatus::Processing,
417435
};
418436

419-
let mut tries = backoff(MAX_RETRY_INTERVAL, self.finalize_timeout);
437+
let mut tries = backoff(MAX_BACKOFF_INTERVAL, self.finalize_timeout);
420438

421439
while order.status == OrderStatus::Processing && wait_for_retry(&res, &mut tries).await {
422440
drop(order);
@@ -466,7 +484,7 @@ where
466484
return Err(NewCertificateError::ChallengeStatus(result.status));
467485
}
468486

469-
let mut tries = backoff(MAX_RETRY_INTERVAL, self.authorization_timeout);
487+
let mut tries = backoff(MAX_BACKOFF_INTERVAL, self.authorization_timeout);
470488
wait_for_retry(&res, &mut tries).await;
471489

472490
let result = loop {
@@ -552,7 +570,8 @@ async fn wait_for_retry<B>(
552570
.headers()
553571
.get(http::header::RETRY_AFTER)
554572
.and_then(parse_retry_after)
555-
.unwrap_or(interval);
573+
.unwrap_or(interval)
574+
.min(MAX_SERVER_RETRY_INTERVAL);
556575

557576
sleep(retry_after).await;
558577
true

src/acme/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// LICENSE file in the root directory of this source tree.
55

66
use core::error::Error as StdError;
7+
use core::time::Duration;
78

89
use ngx::allocator::{unsize_box, Box};
910
use thiserror::Error;
@@ -133,6 +134,9 @@ pub enum RequestError {
133134
#[error(transparent)]
134135
Protocol(#[from] Problem),
135136

137+
#[error("rate limit exceeded, next attempt in {0:?}")]
138+
RateLimited(Duration),
139+
136140
#[error("cannot serialize request ({0})")]
137141
RequestFormat(serde_json::Error),
138142

src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use openssl::x509::X509;
2323
use time::TimeRange;
2424
use zeroize::Zeroizing;
2525

26+
use crate::acme::error::RequestError;
2627
use crate::acme::AcmeClient;
2728
use crate::conf::{AcmeMainConfig, AcmeServerConfig, NGX_HTTP_ACME_COMMANDS};
2829
use crate::net::http::NgxHttpClient;
@@ -312,6 +313,15 @@ async fn ngx_http_acme_update_certificates_for_issuer(
312313
issuer.set_invalid(&err);
313314
return Ok(Time::MAX);
314315
}
316+
Err(acme::error::NewAccountError::Request(err @ RequestError::RateLimited(x))) => {
317+
ngx_log_error!(
318+
NGX_LOG_WARN,
319+
log.as_ptr(),
320+
"{err} while creating account for acme issuer \"{issuer}\"",
321+
issuer = issuer.name
322+
);
323+
return Ok(Time::now() + x);
324+
}
315325
Err(err) => {
316326
ngx_log_error!(
317327
NGX_LOG_WARN,
@@ -373,6 +383,15 @@ async fn ngx_http_acme_update_certificates_for_issuer(
373383

374384
next
375385
}
386+
Err(acme::error::NewCertificateError::Request(err @ RequestError::RateLimited(x))) => {
387+
ngx_log_error!(
388+
NGX_LOG_WARN,
389+
log.as_ptr(),
390+
"{err} while updating certificate \"{issuer}/{order_id}\"",
391+
issuer = issuer.name,
392+
);
393+
return Ok(Time::now() + x);
394+
}
376395
Err(ref err) if err.is_invalid() => {
377396
ngx_log_error!(
378397
NGX_LOG_ERR,

0 commit comments

Comments
 (0)