Skip to content

Commit e3dffdb

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 70fdf8d commit e3dffdb

File tree

3 files changed

+56
-8
lines changed

3 files changed

+56
-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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,18 @@ async fn ngx_http_acme_update_certificates_for_issuer(
313313
issuer.set_invalid(&err);
314314
return Ok(Time::MAX);
315315
}
316+
Err(acme::error::NewAccountError::Request(
317+
acme::error::RequestError::RateLimited(x),
318+
)) => {
319+
ngx_log_error!(
320+
NGX_LOG_WARN,
321+
log.as_ptr(),
322+
"rate limit exceeded while registering accoung for issuer \"{}\", next attempt in {:?}",
323+
issuer.name,
324+
x
325+
);
326+
return Ok(Time::now() + x);
327+
}
316328
Err(err) => {
317329
ngx_log_error!(
318330
NGX_LOG_WARN,
@@ -379,6 +391,19 @@ async fn ngx_http_acme_update_certificates_for_issuer(
379391

380392
next
381393
}
394+
Err(acme::error::NewCertificateError::Request(
395+
acme::error::RequestError::RateLimited(x),
396+
)) => {
397+
ngx_log_error!(
398+
NGX_LOG_WARN,
399+
log.as_ptr(),
400+
"rate limit exceeded while updating acme certificate \"{}/{}\", next attempt in {:?}",
401+
issuer.name,
402+
order.cache_key(),
403+
x
404+
);
405+
return Ok(Time::now() + x);
406+
}
382407
Err(ref err) if err.is_invalid() => {
383408
ngx_log_error!(
384409
NGX_LOG_ERR,

0 commit comments

Comments
 (0)