From 2ab6e9cb9e9f31e4516b96ac44cde3629cfc3cee Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Thu, 22 Oct 2026 06:20:48 +0200 Subject: [PATCH] mod_md: Honour the Retry-After header of ACME responses. Add ap_parse_retry_after() to parse the value of a Retry-After header, either delay-seconds or an HTTP-date as defined in RFC 9110, section 5.6.7. mod_md uses it as RFC 8555 recommends: when polling orders and authorizations, after finalizing an order and when a request is rate limited. A hint beyond the timeout is waited for only until the deadline, and the wait ends early on shutdown. a2md does not link httpd and carries its own copy of the parser. PR 70282. --- changes-entries/70282.txt | 3 + changes-entries/md-retry-after.txt | 4 + include/ap_mmn.h | 3 +- include/httpd.h | 25 +++ modules/md/md_acme.c | 256 +++++++++++++++++++++++++++++ modules/md/md_acme.h | 8 + modules/md/md_acme_authz.c | 37 ++++- modules/md/md_acme_authz.h | 1 + modules/md/md_acme_drive.c | 2 + modules/md/md_acme_order.c | 21 ++- modules/md/md_acme_order.h | 1 + modules/md/md_acmev2_drive.c | 12 +- modules/md/md_result.c | 2 + modules/md/md_result.h | 1 + modules/md/md_status.c | 18 +- modules/md/md_util.c | 93 ++++++++++- modules/md/md_util.h | 27 +++ modules/md/mod_md_drive.c | 20 +++ server/util.c | 236 ++++++++++++++++++++++++++ test/unit/util.c | 181 ++++++++++++++++++++ 20 files changed, 934 insertions(+), 17 deletions(-) create mode 100644 changes-entries/70282.txt create mode 100644 changes-entries/md-retry-after.txt diff --git a/changes-entries/70282.txt b/changes-entries/70282.txt new file mode 100644 index 00000000000..738b0362197 --- /dev/null +++ b/changes-entries/70282.txt @@ -0,0 +1,3 @@ + *) core: Add ap_parse_retry_after() to parse the value of a Retry-After + header field, either delay-seconds or an HTTP-date. PR 70282 + [Arturo Bernal] diff --git a/changes-entries/md-retry-after.txt b/changes-entries/md-retry-after.txt new file mode 100644 index 00000000000..63de6bc7f21 --- /dev/null +++ b/changes-entries/md-retry-after.txt @@ -0,0 +1,4 @@ + *) mod_md: Honour the Retry-After header of ACME responses, as RFC 8555 + recommends, when polling orders and authorizations, after finalizing an + order and when a request is rate limited. PR 70282 + [Arturo Bernal] diff --git a/include/ap_mmn.h b/include/ap_mmn.h index b69d07b74af..f6d10c5284d 100644 --- a/include/ap_mmn.h +++ b/include/ap_mmn.h @@ -739,6 +739,7 @@ * 20211221.31 (2.5.1-dev) Add ap_*_timingsafe() to httpd.h * 20211221.32 (2.5.1-dev) Add ap_cstr_stripws() to httpd.h * 20211221.33 (2.5.1-dev) Add ap_size_parameter_parse() to httpd.h + * 20211221.34 (2.5.1-dev) Add ap_parse_retry_after() to httpd.h */ #define MODULE_MAGIC_COOKIE 0x41503235UL /* "AP25" */ @@ -746,7 +747,7 @@ #ifndef MODULE_MAGIC_NUMBER_MAJOR #define MODULE_MAGIC_NUMBER_MAJOR 20211221 #endif -#define MODULE_MAGIC_NUMBER_MINOR 33 /* 0...n */ +#define MODULE_MAGIC_NUMBER_MINOR 34 /* 0...n */ /** * Determine if the server's current MODULE_MAGIC_NUMBER is at least a diff --git a/include/httpd.h b/include/httpd.h index 8c9d24b6c6b..d6fbc9169b2 100644 --- a/include/httpd.h +++ b/include/httpd.h @@ -2340,6 +2340,31 @@ AP_DECLARE(char *) ap_append_pid(apr_pool_t *p, const char *string, */ AP_DECLARE(int) ap_parse_strict_length(apr_off_t *len, const char *str); +/** + * Parse the value of a Retry-After header field (RFC 9110, section 10.2.3), + * which is either a number of seconds or an HTTP-date. Leading and trailing + * whitespace is ignored. Anything else, such as a sign, a fraction, a list + * of values, a time zone other than GMT or text after the date, is invalid. + * An HTTP-date is accepted in the three formats of RFC 9110, section 5.6.7. + * The day name is checked for its syntax only. A delay too large for an + * apr_interval_time_t is saturated to APR_INT64_MAX. + * + * An HTTP-date is relative to the clock of the server that sent it. To + * be independent of any difference to the local clock, the delay is computed + * against the Date header field of the same response, if given. + * @param delay The parsed delay, never negative + * @param value The field value to parse + * @param date The value of the Date header field of the response, or NULL. + * It is ignored if it is not a valid HTTP-date. + * @param now The local time to measure an HTTP-date against if date is not + * usable, also to resolve a two digit year + * @return APR_SUCCESS, or APR_EINVAL if value is not valid + */ +AP_DECLARE(apr_status_t) ap_parse_retry_after(apr_interval_time_t *delay, + const char *value, + const char *date, + apr_time_t now); + /** * Parse a given timeout parameter string into an apr_interval_time_t value. * The unit of the time interval is given as postfix string to the numeric diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c index 60940f04f08..ba79ef435ca 100644 --- a/modules/md/md_acme.c +++ b/modules/md/md_acme.c @@ -168,12 +168,267 @@ apr_status_t md_acme_init(apr_pool_t *p, const char *base, int init_ssl) return init_ssl? md_crypt_init(p) : APR_SUCCESS; } +#ifdef MD_A2MD_BUILD +/* a2md does not link httpd, so it cannot call ap_parse_retry_after(). This is + * the same code, keep both in sync. */ +/* HTTP-date as in RFC 9110, section 5.6.7: IMF-fixdate, rfc850-date or + * asctime-date. Unlike apr_date_parse_http() the whole value must match. */ +static int ra_digits(const char *s, int n) +{ + int v = 0; + + while (n-- > 0) { + if (*s < '0' || *s > '9') { + return -1; + } + v = v * 10 + (*s++ - '0'); + } + return v; +} + +static int ra_month(const char *s) +{ + static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + int i; + + for (i = 0; i < 12; ++i) { + if (!memcmp(s, months + 3 * i, 3)) { + return i; + } + } + return -1; +} + +/* day-name, or day-name-l if len is more than 3, must match case sensitively */ +static int ra_day_name(const char *s, apr_size_t len) +{ + static const char *const names[] = { + "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" + }; + int i; + + for (i = 0; i < 7; ++i) { + if (strlen(names[i]) == len ? !memcmp(s, names[i], len) + : (len == 3 && !memcmp(s, names[i], 3))) { + return 1; + } + } + return 0; +} + +static int ra_time_of_day(const char *s, int *h, int *m, int *sec) +{ + if (s[2] != ':' || s[5] != ':') { + return 0; + } + *h = ra_digits(s, 2); + *m = ra_digits(s + 3, 2); + *sec = ra_digits(s + 6, 2); + /* a leap second, 60, is syntactically valid */ + return *h >= 0 && *h < 24 && *m >= 0 && *m < 60 && *sec >= 0 && *sec <= 60; +} + +static apr_int64_t ra_days_from_civil(apr_int64_t y, int m, int d) +{ + apr_int64_t era, yoe, doy; + + y -= m <= 2; + era = (y >= 0 ? y : y - 399) / 400; + yoe = y - era * 400; + doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + return era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468; +} + +static int ra_date_ok(int year, int mon, int mday) +{ + static const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + int leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; + + return mday >= 1 && mday <= days[mon] + (mon == 1 && leap); +} + +/* The year of t, using the proleptic Gregorian calendar. */ +static apr_int64_t ra_year_of(apr_time_t t) +{ + apr_int64_t secs = t / APR_USEC_PER_SEC; + apr_int64_t days, era, doe, yoe, doy; + + if (t % APR_USEC_PER_SEC < 0) { + --secs; + } + days = secs / 86400; + if (secs % 86400 < 0) { + --days; + } + days += 719468; + era = (days >= 0 ? days : days - 146096) / 146097; + doe = days - era * 146097; + yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + return yoe + era * 400 + ((5 * doy + 2) / 153 >= 10); +} + +static int ra_http_date(const char *s, apr_size_t len, apr_time_t now, + apr_time_t *when) +{ + int year, mon, mday, h, m, sec; + const char *tod; + apr_size_t comma; + + if (len == 29 && s[3] == ',') { + /* IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT */ + if (!ra_day_name(s, 3) || s[4] != ' ' || s[7] != ' ' || s[11] != ' ' + || s[16] != ' ' || memcmp(s + 25, " GMT", 4)) { + return 0; + } + mday = ra_digits(s + 5, 2); + mon = ra_month(s + 8); + year = ra_digits(s + 12, 4); + tod = s + 17; + } + else if (len == 24 && s[3] == ' ') { + /* asctime-date: Sun Nov 6 08:49:37 1994 */ + if (!ra_day_name(s, 3) || s[7] != ' ' || s[10] != ' ' || s[19] != ' ') { + return 0; + } + mon = ra_month(s + 4); + mday = s[8] == ' ' ? ra_digits(s + 9, 1) : ra_digits(s + 8, 2); + year = ra_digits(s + 20, 4); + tod = s + 11; + } + else { + /* rfc850-date: Sunday, 06-Nov-94 08:49:37 GMT */ + for (comma = 6; comma <= 9 && comma < len && s[comma] != ','; ++comma) + ; + if (comma > 9 || len != comma + 24 || !ra_day_name(s, comma) + || s[comma + 1] != ' ' || s[comma + 4] != '-' || s[comma + 8] != '-' + || s[comma + 11] != ' ' || memcmp(s + comma + 20, " GMT", 4)) { + return 0; + } + mday = ra_digits(s + comma + 2, 2); + mon = ra_month(s + comma + 5); + year = ra_digits(s + comma + 9, 2); + tod = s + comma + 12; + if (year >= 0) { + /* two digit year: not more than 50 years in the future */ + apr_int64_t now_year = ra_year_of(now); + + /* keep the arithmetic below in range for any clock */ + if (now_year < 1970) { + now_year = 1970; + } + else if (now_year > 9999) { + now_year = 9999; + } + year += now_year / 100 * 100; + while (year + 100 <= now_year + 50) { + year += 100; + } + while (year > now_year + 50) { + year -= 100; + } + } + } + if (year < 0 || mon < 0 || mday < 0 || !ra_date_ok(year, mon, mday) + || !ra_time_of_day(tod, &h, &m, &sec)) { + return 0; + } + *when = apr_time_from_sec(ra_days_from_civil(year, mon + 1, mday) * 86400 + + h * 3600 + m * 60 + sec); + return 1; +} + +static const char *ra_trim(const char *s, apr_size_t *len) +{ + while (*s == ' ' || *s == '\t') { + ++s; + } + *len = strlen(s); + while (*len && (s[*len - 1] == ' ' || s[*len - 1] == '\t')) { + --*len; + } + return s; +} + +static apr_status_t retry_after_parse(apr_interval_time_t *delay, const char *value, + const char *date, apr_time_t now) +{ + apr_size_t len, dlen; + apr_time_t when, ref = now; + + if (!value) { + return APR_EINVAL; + } + value = ra_trim(value, &len); + if (!len) { + return APR_EINVAL; + } + + if (*value >= '0' && *value <= '9') { + const apr_int64_t max = APR_INT64_MAX / APR_USEC_PER_SEC; + apr_int64_t secs = 0; + apr_size_t i; + + for (i = 0; i < len; ++i) { + if (value[i] < '0' || value[i] > '9') { + return APR_EINVAL; + } + if (secs <= (max - (value[i] - '0')) / 10) { + secs = secs * 10 + (value[i] - '0'); + } + else { + secs = max + 1; + } + } + *delay = secs > max ? APR_INT64_MAX : apr_time_from_sec(secs); + return APR_SUCCESS; + } + + if (!ra_http_date(value, len, now, &when)) { + return APR_EINVAL; + } + if (date) { + date = ra_trim(date, &dlen); + if (!ra_http_date(date, dlen, now, &ref)) { + ref = now; + } + } + if (when <= ref) { + *delay = 0; + } + else if (ref < 0 && when > APR_INT64_MAX + ref) { + *delay = APR_INT64_MAX; + } + else { + *delay = when - ref; + } + return APR_SUCCESS; +} +#else +#define retry_after_parse ap_parse_retry_after +#endif + +apr_time_t md_acme_retry_after(const struct apr_table_t *headers) +{ + const char *value = headers? apr_table_get(headers, "Retry-After") : NULL; + const char *date = headers? apr_table_get(headers, "Date") : NULL; + apr_time_t now = apr_time_now(); + apr_interval_time_t delay; + + if (!value || APR_SUCCESS != retry_after_parse(&delay, value, date, now)) { + return 0; + } + return (now > 0 && delay > APR_INT64_MAX - now)? APR_INT64_MAX : now + delay; +} + static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res) { const char *ctype; md_json_t *problem = NULL; apr_status_t rv; + /* e.g. rateLimited, RFC 8555 6.6 */ + req->result->retry_at = md_acme_retry_after(req->resp_hdrs); ctype = apr_table_get(req->resp_hdrs, "content-type"); ctype = md_util_parse_ct(res->req->pool, ctype); if (ctype && !strcmp(ctype, "application/problem+json")) { @@ -490,6 +745,7 @@ void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t else { md_result_problem_set(result, acme->last->status, acme->last->problem, acme->last->detail, acme->last->subproblems); + result->retry_at = acme->last->retry_at; } } diff --git a/modules/md/md_acme.h b/modules/md/md_acme.h index 76c27a88dd0..f9797bea3d9 100644 --- a/modules/md/md_acme.h +++ b/modules/md/md_acme.h @@ -168,6 +168,14 @@ apr_status_t md_acme_setup(md_acme_t *acme, struct md_result_t *result); void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result); +/** + * Get the local time a response asks the client to wait for with its + * Retry-After header (RFC 9110, 10.2.3). An HTTP-date is read against the + * Date header of the response, so a skewed clock of the server does not matter. + * @return the time, or 0 if there is no valid Retry-After + */ +apr_time_t md_acme_retry_after(const struct apr_table_t *headers); + /**************************************************************************************************/ /* account handling */ diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c index b155c0a3d6b..7ec22fb4ca7 100644 --- a/modules/md/md_acme_authz.c +++ b/modules/md/md_acme_authz.c @@ -111,6 +111,38 @@ static int copy_challenge_error(void *baton, size_t index, md_json_t *json) return 1; } +typedef struct { + apr_pool_t *p; + md_acme_authz_t *authz; + md_json_t *json; +} authz_get_ctx; + +static apr_status_t on_authz_got(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, + md_json_t *body, void *baton) +{ + authz_get_ctx *ctx = baton; + + (void)acme; + (void)p; + ctx->json = md_json_clone(ctx->p, body); + ctx->authz->retry_at = md_acme_retry_after(hdrs); + return APR_SUCCESS; +} + +static apr_status_t authz_get_json(md_json_t **pjson, md_acme_authz_t *authz, + md_acme_t *acme, apr_pool_t *p) +{ + authz_get_ctx ctx; + apr_status_t rv; + + ctx.p = p; + ctx.authz = authz; + ctx.json = NULL; + rv = md_acme_GET(acme, authz->url, NULL, on_authz_got, NULL, NULL, 1, &ctx); + *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; + return rv; +} + apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_pool_t *p) { md_json_t *json; @@ -131,7 +163,8 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p err = "unable to parse response"; log_level = MD_LOG_ERR; - if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, 1, p)) + authz->retry_at = 0; + if (APR_SUCCESS == (rv = authz_get_json(&json, authz, acme, p)) && (s = md_json_gets(json, MD_KEY_STATUS, NULL))) { authz->domain = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); @@ -215,8 +248,8 @@ static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_tab (void)acme; (void)p; - (void)hdrs; (void)body; + ctx->authz->retry_at = md_acme_retry_after(hdrs); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ctx->p, "updated authz %s", ctx->authz->url); return APR_SUCCESS; } diff --git a/modules/md/md_acme_authz.h b/modules/md/md_acme_authz.h index d74beebab68..74349bb040d 100644 --- a/modules/md/md_acme_authz.h +++ b/modules/md/md_acme_authz.h @@ -53,6 +53,7 @@ struct md_acme_authz_t { const char *error_detail; const struct md_json_t *error_subproblems; struct md_json_t *resource; + apr_time_t retry_at; /* not persisted: Retry-After of the last response */ }; #define MD_FN_HTTP01 "acme-http-01.txt" diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index bef07e52d91..f51b30255ce 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -304,6 +304,8 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void apr_status_t rv = APR_SUCCESS; (void)acme; + /* the order may still be processing, the CA says when to look again */ + ad->order->retry_at = md_acme_retry_after(res->headers); location = apr_table_get(res->headers, "location"); if (!location) return rv; diff --git a/modules/md/md_acme_order.c b/modules/md/md_acme_order.c index d3451dcd26a..29ebba292c0 100644 --- a/modules/md/md_acme_order.c +++ b/modules/md/md_acme_order.c @@ -267,12 +267,13 @@ typedef struct { const char *ari_cert_id; apr_array_header_t *domains; md_result_t *result; + apr_time_t retry_at; } order_ctx_t; #define ORDER_CTX_INIT(ctx, p, o, a, n, d, pf, cid, r) \ (ctx)->p = (p); (ctx)->order = (o); (ctx)->acme = (a); \ (ctx)->name = (n); (ctx)->domains = d; (ctx)->profile = pf; \ - (ctx)->ari_cert_id = cid;(ctx)->result = r + (ctx)->ari_cert_id = cid;(ctx)->result = r; (ctx)->retry_at = 0 static apr_status_t identifier_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) { @@ -323,6 +324,7 @@ static apr_status_t on_order_upd(md_acme_t *acme, apr_pool_t *p, const apr_table } order_update_from_json(ctx->order, body, ctx->p); + ctx->order->retry_at = md_acme_retry_after(hdrs); out: return rv; } @@ -391,7 +393,8 @@ apr_status_t md_acme_order_await_ready(md_acme_order_t *order, md_acme_t *acme, ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, NULL, result); md_result_activity_setn(result, "Waiting for order to become ready"); - rv = md_util_try(await_ready, &ctx, 0, timeout, 0, 0, 1); + rv = md_util_try_with_retry_after(await_ready, &ctx, 0, timeout, 0, 0, 1, + &order->retry_at); md_result_log(result, MD_LOG_DEBUG); return rv; } @@ -434,7 +437,8 @@ apr_status_t md_acme_order_await_valid(md_acme_order_t *order, md_acme_t *acme, ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, NULL, result); md_result_activity_setn(result, "Waiting for finalized order to become valid"); - rv = md_util_try(await_valid, &ctx, 0, timeout, 0, 0, 1); + rv = md_util_try_with_retry_after(await_valid, &ctx, 0, timeout, 0, 0, 1, + &order->retry_at); md_result_log(result, MD_LOG_DEBUG); return rv; } @@ -480,6 +484,9 @@ apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *a if (APR_SUCCESS != rv) { goto leave; } + if (authz->retry_at > order->retry_at) { + order->retry_at = authz->retry_at; + } add_setup_token(order, setup_token); md_acme_order_save(store, p, MD_SG_STAGING, md->name, order, 0); break; @@ -511,6 +518,7 @@ static apr_status_t check_challenges(void *baton, int attempt) apr_status_t rv = APR_SUCCESS; int i; + ctx->retry_at = 0; for (i = 0; i < ctx->order->authz_urls->nelts; ++i) { url = APR_ARRAY_IDX(ctx->order->authz_urls, i, const char*); md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ctx->p, "%s: check AUTHZ at %s (attempt %d)", @@ -525,6 +533,7 @@ static apr_status_t check_challenges(void *baton, int attempt) break; case MD_ACME_AUTHZ_S_PENDING: rv = APR_EAGAIN; + ctx->retry_at = authz->retry_at; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ctx->p, "%s: status pending at %s", authz->domain, authz->url); goto leave; @@ -562,9 +571,13 @@ apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acm apr_status_t rv; ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, NULL, result); + ctx.retry_at = order->retry_at; md_result_activity_printf(result, "Monitoring challenge status for %s", md->name); - rv = md_util_try(check_challenges, &ctx, 0, timeout, 0, 0, 1); + rv = md_util_try_with_retry_after(check_challenges, &ctx, 0, timeout, 0, 0, 1, + &ctx.retry_at); + /* that was the hint for the authorizations, not for the order */ + order->retry_at = 0; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: checked authorizations", md->name); return rv; } diff --git a/modules/md/md_acme_order.h b/modules/md/md_acme_order.h index 2d08cd9861a..4c132a554c9 100644 --- a/modules/md/md_acme_order.h +++ b/modules/md/md_acme_order.h @@ -33,6 +33,7 @@ struct md_acme_order_t { apr_pool_t *p; const char *url; md_acme_order_st status; + apr_time_t retry_at; /* not persisted: Retry-After of the last response */ struct apr_array_header_t *authz_urls; struct apr_array_header_t *challenge_setups; struct md_json_t *json; diff --git a/modules/md/md_acmev2_drive.c b/modules/md/md_acmev2_drive.c index facb080f5e3..b84c42e2943 100644 --- a/modules/md/md_acmev2_drive.c +++ b/modules/md/md_acmev2_drive.c @@ -148,6 +148,12 @@ static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, in /**************************************************************************************************/ /* ACMEv2 renewal */ +/* A wait cut short by a shutdown is not a failed renewal, do not report it as one. */ +static void wait_failed(md_result_t *result, apr_status_t rv, const char *msg) +{ + md_result_set(result, rv, APR_STATUS_IS_EINTR(rv)? "Interrupted by shutdown" : msg); +} + apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, md_result_t *result) { apr_status_t rv = APR_SUCCESS; @@ -208,14 +214,14 @@ apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m rv = md_acme_order_monitor_authzs(ad->order, ad->acme, d->md, ad->authz_monitor_timeout, result, d->p); if (APR_SUCCESS != rv) { - md_result_set(result, rv, "Error waiting on domain names to be validated"); + wait_failed(result, rv, "Error waiting on domain names to be validated"); goto leave; } rv = md_acme_order_await_ready(ad->order, ad->acme, d->md, ad->authz_monitor_timeout, result, d->p); if (APR_SUCCESS != rv) { - md_result_set(result, rv, "Error waiting for order to become ready"); + wait_failed(result, rv, "Error waiting for order to become ready"); goto leave; } @@ -228,7 +234,7 @@ apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m rv = md_acme_order_await_valid(ad->order, ad->acme, d->md, ad->authz_monitor_timeout, result, d->p); if (APR_SUCCESS != rv) { - md_result_set(result, rv, "Error waiting for order to become valid."); + wait_failed(result, rv, "Error waiting for order to become valid."); goto leave; } diff --git a/modules/md/md_result.c b/modules/md/md_result.c index 64a2f70bac8..c156c71d72d 100644 --- a/modules/md/md_result.c +++ b/modules/md/md_result.c @@ -210,6 +210,7 @@ void md_result_assign(md_result_t *dest, const md_result_t *src) dest->detail = src->detail; dest->activity = src->activity; dest->ready_at = src->ready_at; + dest->retry_at = src->retry_at; dest->subproblems = src->subproblems; } @@ -220,6 +221,7 @@ void md_result_dup(md_result_t *dest, const md_result_t *src) dest->detail = src->detail? apr_pstrdup(dest->p, src->detail) : NULL; dest->activity = src->activity? apr_pstrdup(dest->p, src->activity) : NULL; dest->ready_at = src->ready_at; + dest->retry_at = src->retry_at; dest->subproblems = src->subproblems? md_json_clone(dest->p, src->subproblems) : NULL; on_change(dest); } diff --git a/modules/md/md_result.h b/modules/md/md_result.h index e83bdd22236..0f57e468cf2 100644 --- a/modules/md/md_result.h +++ b/modules/md/md_result.h @@ -35,6 +35,7 @@ struct md_result_t { const struct md_json_t *subproblems; const char *activity; apr_time_t ready_at; + apr_time_t retry_at; /* earliest time the server asked us to come back, or 0 */ md_result_change_cb *on_change; void *on_change_data; md_result_raise_cb *on_raise; diff --git a/modules/md/md_status.c b/modules/md/md_status.c index 096503109e8..e7bc586e5b3 100644 --- a/modules/md/md_status.c +++ b/modules/md/md_status.c @@ -680,10 +680,26 @@ void md_job_end_run(md_job_t *job, md_result_t *result) job->dirty = 1; md_job_log_append(job, "finished", NULL, NULL); } + else if (APR_STATUS_IS_EINTR(result->status)) { + /* Cancelled by a shutdown, not a failed run: no error is counted + * and no delay applies, the next process starts over. */ + job->next_run = 0; + job->dirty = 1; + } else { + apr_time_t now = apr_time_now(); + apr_time_t retry_at = result->retry_at; + ++job->error_runs; job->dirty = 1; - job->next_run = apr_time_now() + md_job_delay_on_errors(job, job->error_runs, result->problem); + job->next_run = now + md_job_delay_on_errors(job, job->error_runs, result->problem); + /* honour a Retry-After of the CA, but not for longer than we ever back off */ + if (retry_at > now + apr_time_from_sec(24*60*60)) { + retry_at = now + apr_time_from_sec(24*60*60); + } + if (retry_at > job->next_run) { + job->next_run = retry_at; + } } job_observation_end(job); } diff --git a/modules/md/md_util.c b/modules/md/md_util.c index f55d83fb676..7f971d11cc0 100644 --- a/modules/md/md_util.c +++ b/modules/md/md_util.c @@ -1045,9 +1045,65 @@ apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const ch /* try and retry for a while **********************************************************************/ +static md_util_interrupt_fn *interrupt_fn; +static void *interrupt_baton; + +void md_util_interrupt_set(md_util_interrupt_fn *fn, void *baton) +{ + interrupt_fn = fn; + interrupt_baton = baton; +} + +static int interrupted(void) +{ + return interrupt_fn && interrupt_fn(interrupt_baton); +} + +/* Sleep in slices, so a long Retry-After does not delay a shutdown. */ +static apr_status_t interruptible_sleep(apr_interval_time_t duration) +{ + const apr_interval_time_t slice = apr_time_from_msec(100); + + while (duration > 0) { + if (interrupted()) { + return APR_EINTR; + } + apr_sleep(duration < slice? duration : slice); + duration -= slice; + } + return interrupted()? APR_EINTR : APR_SUCCESS; +} + +static apr_status_t try_loop(md_util_try_fn *fn, void *baton, int ignore_errs, + apr_interval_time_t timeout, + apr_interval_time_t start_delay, + apr_interval_time_t max_delay, int backoff, + apr_time_t *retry_at, int interruptible); + apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, apr_interval_time_t timeout, apr_interval_time_t start_delay, apr_interval_time_t max_delay, int backoff) +{ + return try_loop(fn, baton, ignore_errs, timeout, start_delay, max_delay, + backoff, NULL, 0); +} + +apr_status_t md_util_try_with_retry_after(md_util_try_fn *fn, void *baton, + int ignore_errs, + apr_interval_time_t timeout, + apr_interval_time_t start_delay, + apr_interval_time_t max_delay, + int backoff, apr_time_t *retry_at) +{ + return try_loop(fn, baton, ignore_errs, timeout, start_delay, max_delay, + backoff, retry_at, 1); +} + +static apr_status_t try_loop(md_util_try_fn *fn, void *baton, int ignore_errs, + apr_interval_time_t timeout, + apr_interval_time_t start_delay, + apr_interval_time_t max_delay, int backoff, + apr_time_t *retry_at, int interruptible) { apr_status_t rv; apr_time_t now = apr_time_now(); @@ -1056,21 +1112,40 @@ apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, apr_interval_time_t nap_max = max_delay? max_delay : apr_time_from_sec(10); apr_interval_time_t left; int i = 0; - + while (1) { + /* A hint beyond the deadline is waited for only until the deadline, so + * that a server cannot make us give up before the timeout is over. */ + if (retry_at && *retry_at > now) { + apr_interval_time_t wait = (*retry_at < giveup? *retry_at : giveup) - now; + + if (interruptible) { + if (APR_SUCCESS != (rv = interruptible_sleep(wait))) break; + } + else { + apr_sleep(wait); + } + } + else if (interruptible && interrupted()) { + rv = APR_EINTR; + break; + } if (APR_SUCCESS == (rv = fn(baton, i++))) { break; } else if (!APR_STATUS_IS_EAGAIN(rv) && !ignore_errs) { break; } - + now = apr_time_now(); if (now > giveup) { rv = APR_TIMEUP; break; } - + if (retry_at && *retry_at > now && now < giveup) { + continue; + } + left = giveup - now; if (nap_duration > left) { nap_duration = left; @@ -1078,11 +1153,17 @@ apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, if (nap_duration > nap_max) { nap_duration = nap_max; } - - apr_sleep(nap_duration); + + if (interruptible) { + if (APR_SUCCESS != (rv = interruptible_sleep(nap_duration))) break; + } + else { + apr_sleep(nap_duration); + } if (backoff) { nap_duration *= 2; - } + } + now = apr_time_now(); } return rv; } diff --git a/modules/md/md_util.h b/modules/md/md_util.h index 51f3d602c4d..ca82711f242 100644 --- a/modules/md/md_util.h +++ b/modules/md/md_util.h @@ -288,4 +288,31 @@ apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, apr_interval_time_t timeout, apr_interval_time_t start_delay, apr_interval_time_t max_delay, int backoff); +/** + * Callback to tell md_util_try_with_retry_after() to stop waiting, e.g. + * because the process is shutting down. Return non-zero to cancel. + */ +typedef int md_util_interrupt_fn(void *baton); + +/** + * Register the callback polled by md_util_try_with_retry_after() while it + * waits. Not set by default, so a waiting a2md is not interrupted. + */ +void md_util_interrupt_set(md_util_interrupt_fn *fn, void *baton); + +/** + * Like md_util_try(), but honours a server given Retry-After. *retry_at is + * the local time before which fn should not be called again. It is read + * before every call, so fn may set it, and is waited for at most until the + * timeout, then fn is called a last time. May be NULL. The waits end early + * and APR_EINTR is returned, without calling fn again, when the callback + * set by md_util_interrupt_set() asks for it. + */ +apr_status_t md_util_try_with_retry_after(md_util_try_fn *fn, void *baton, + int ignore_errs, + apr_interval_time_t timeout, + apr_interval_time_t start_delay, + apr_interval_time_t max_delay, + int backoff, apr_time_t *retry_at); + #endif /* md_util_h */ diff --git a/modules/md/mod_md_drive.c b/modules/md/mod_md_drive.c index 14e3725b094..ae966656c09 100644 --- a/modules/md/mod_md_drive.c +++ b/modules/md/mod_md_drive.c @@ -26,6 +26,7 @@ #include #include +#include "ap_mpm.h" #include "mod_watchdog.h" #include "md.h" @@ -68,6 +69,15 @@ struct md_renew_ctx_t { apr_array_header_t *jobs; }; +static int server_is_stopping(void *baton) +{ + int state; + + (void)baton; + return APR_SUCCESS == ap_mpm_query(AP_MPMQ_MPM_STATE, &state) + && AP_MPMQ_STOPPING == state; +} + static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *ptemp) { const md_t *md; @@ -202,6 +212,15 @@ static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *p md_job_notify(job, "renewed", result); } } + else if (APR_STATUS_IS_EINTR(result->status)) { + /* Not an error, so neither logged nor reported as one. Leave the + * last result in the job as it was. */ + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, dctx->s, + "md(%s): renewal interrupted by shutdown", job->mdomain); + if (job->last_result) md_result_assign(result, job->last_result); + else md_result_set(result, APR_SUCCESS, NULL); + goto leave; + } else { ap_log_error( APLOG_MARK, APLOG_ERR, result->status, dctx->s, APLOGNO(10056) "processing %s: %s", job->mdomain, result->detail); @@ -263,6 +282,7 @@ static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) */ switch (state) { case AP_WATCHDOG_STATE_STARTING: + md_util_interrupt_set(server_is_stopping, NULL); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10054) "md watchdog start, auto drive %d mds", dctx->jobs->nelts); break; diff --git a/server/util.c b/server/util.c index 471caeb6a66..99775008add 100644 --- a/server/util.c +++ b/server/util.c @@ -2832,6 +2832,242 @@ AP_DECLARE(int) ap_parse_strict_length(apr_off_t *len, const char *str) && *end == '\0'); } +/* HTTP-date as in RFC 9110, section 5.6.7: IMF-fixdate, rfc850-date or + * asctime-date. Unlike apr_date_parse_http() the whole value must match. */ +static int ra_digits(const char *s, int n) +{ + int v = 0; + + while (n-- > 0) { + if (*s < '0' || *s > '9') { + return -1; + } + v = v * 10 + (*s++ - '0'); + } + return v; +} + +static int ra_month(const char *s) +{ + static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + int i; + + for (i = 0; i < 12; ++i) { + if (!memcmp(s, months + 3 * i, 3)) { + return i; + } + } + return -1; +} + +/* day-name, or day-name-l if len is more than 3, must match case sensitively */ +static int ra_day_name(const char *s, apr_size_t len) +{ + static const char *const names[] = { + "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" + }; + int i; + + for (i = 0; i < 7; ++i) { + if (strlen(names[i]) == len ? !memcmp(s, names[i], len) + : (len == 3 && !memcmp(s, names[i], 3))) { + return 1; + } + } + return 0; +} + +static int ra_time_of_day(const char *s, int *h, int *m, int *sec) +{ + if (s[2] != ':' || s[5] != ':') { + return 0; + } + *h = ra_digits(s, 2); + *m = ra_digits(s + 3, 2); + *sec = ra_digits(s + 6, 2); + /* a leap second, 60, is syntactically valid */ + return *h >= 0 && *h < 24 && *m >= 0 && *m < 60 && *sec >= 0 && *sec <= 60; +} + +static apr_int64_t ra_days_from_civil(apr_int64_t y, int m, int d) +{ + apr_int64_t era, yoe, doy; + + y -= m <= 2; + era = (y >= 0 ? y : y - 399) / 400; + yoe = y - era * 400; + doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + return era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468; +} + +static int ra_date_ok(int year, int mon, int mday) +{ + static const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + int leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; + + return mday >= 1 && mday <= days[mon] + (mon == 1 && leap); +} + +/* The year of t, using the proleptic Gregorian calendar. */ +static apr_int64_t ra_year_of(apr_time_t t) +{ + apr_int64_t secs = t / APR_USEC_PER_SEC; + apr_int64_t days, era, doe, yoe, doy; + + if (t % APR_USEC_PER_SEC < 0) { + --secs; + } + days = secs / 86400; + if (secs % 86400 < 0) { + --days; + } + days += 719468; + era = (days >= 0 ? days : days - 146096) / 146097; + doe = days - era * 146097; + yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + return yoe + era * 400 + ((5 * doy + 2) / 153 >= 10); +} + +static int ra_http_date(const char *s, apr_size_t len, apr_time_t now, + apr_time_t *when) +{ + int year, mon, mday, h, m, sec; + const char *tod; + apr_size_t comma; + + if (len == 29 && s[3] == ',') { + /* IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT */ + if (!ra_day_name(s, 3) || s[4] != ' ' || s[7] != ' ' || s[11] != ' ' + || s[16] != ' ' || memcmp(s + 25, " GMT", 4)) { + return 0; + } + mday = ra_digits(s + 5, 2); + mon = ra_month(s + 8); + year = ra_digits(s + 12, 4); + tod = s + 17; + } + else if (len == 24 && s[3] == ' ') { + /* asctime-date: Sun Nov 6 08:49:37 1994 */ + if (!ra_day_name(s, 3) || s[7] != ' ' || s[10] != ' ' || s[19] != ' ') { + return 0; + } + mon = ra_month(s + 4); + mday = s[8] == ' ' ? ra_digits(s + 9, 1) : ra_digits(s + 8, 2); + year = ra_digits(s + 20, 4); + tod = s + 11; + } + else { + /* rfc850-date: Sunday, 06-Nov-94 08:49:37 GMT */ + for (comma = 6; comma <= 9 && comma < len && s[comma] != ','; ++comma) + ; + if (comma > 9 || len != comma + 24 || !ra_day_name(s, comma) + || s[comma + 1] != ' ' || s[comma + 4] != '-' || s[comma + 8] != '-' + || s[comma + 11] != ' ' || memcmp(s + comma + 20, " GMT", 4)) { + return 0; + } + mday = ra_digits(s + comma + 2, 2); + mon = ra_month(s + comma + 5); + year = ra_digits(s + comma + 9, 2); + tod = s + comma + 12; + if (year >= 0) { + /* two digit year: not more than 50 years in the future */ + apr_int64_t now_year = ra_year_of(now); + + /* keep the arithmetic below in range for any clock */ + if (now_year < 1970) { + now_year = 1970; + } + else if (now_year > 9999) { + now_year = 9999; + } + year += now_year / 100 * 100; + while (year + 100 <= now_year + 50) { + year += 100; + } + while (year > now_year + 50) { + year -= 100; + } + } + } + if (year < 0 || mon < 0 || mday < 0 || !ra_date_ok(year, mon, mday) + || !ra_time_of_day(tod, &h, &m, &sec)) { + return 0; + } + *when = apr_time_from_sec(ra_days_from_civil(year, mon + 1, mday) * 86400 + + h * 3600 + m * 60 + sec); + return 1; +} + +static const char *ra_trim(const char *s, apr_size_t *len) +{ + while (*s == ' ' || *s == '\t') { + ++s; + } + *len = strlen(s); + while (*len && (s[*len - 1] == ' ' || s[*len - 1] == '\t')) { + --*len; + } + return s; +} + +AP_DECLARE(apr_status_t) ap_parse_retry_after(apr_interval_time_t *delay, + const char *value, + const char *date, + apr_time_t now) +{ + apr_size_t len, dlen; + apr_time_t when, ref = now; + + if (!value) { + return APR_EINVAL; + } + value = ra_trim(value, &len); + if (!len) { + return APR_EINVAL; + } + + if (*value >= '0' && *value <= '9') { + const apr_int64_t max = APR_INT64_MAX / APR_USEC_PER_SEC; + apr_int64_t secs = 0; + apr_size_t i; + + for (i = 0; i < len; ++i) { + if (value[i] < '0' || value[i] > '9') { + return APR_EINVAL; + } + if (secs <= (max - (value[i] - '0')) / 10) { + secs = secs * 10 + (value[i] - '0'); + } + else { + secs = max + 1; + } + } + *delay = secs > max ? APR_INT64_MAX : apr_time_from_sec(secs); + return APR_SUCCESS; + } + + if (!ra_http_date(value, len, now, &when)) { + return APR_EINVAL; + } + if (date) { + date = ra_trim(date, &dlen); + if (!ra_http_date(date, dlen, now, &ref)) { + ref = now; + } + } + if (when <= ref) { + *delay = 0; + } + else if (ref < 0 && when > APR_INT64_MAX + ref) { + *delay = APR_INT64_MAX; + } + else { + *delay = when - ref; + } + return APR_SUCCESS; +} + /** * Determine if a request has a request body or not. * diff --git a/test/unit/util.c b/test/unit/util.c index 3cca7c2b19f..1d43c95fe60 100644 --- a/test/unit/util.c +++ b/test/unit/util.c @@ -218,6 +218,187 @@ HTTPD_START_LOOP_TEST(size_parameter_parse_handles_suffixes, ap_size_parameter_p END_TEST +/* + * ap_parse_retry_after() + */ + +#define RA_DATE "Sun, 06 Nov 1994 08:49:37 GMT" +#define RA_BASE APR_INT64_C(784111777) /* RA_DATE in seconds since the epoch */ +#define RA_MAX (-1) /* saturated to APR_INT64_MAX */ + +struct ap_parse_retry_after_case { + const char *input; + const char *date; /* Date header of the response or NULL */ + apr_int64_t now; /* local time in seconds */ + apr_status_t expected_rv; + apr_int64_t expected; /* delay in seconds */ +}; + +const struct ap_parse_retry_after_case ap_parse_retry_after_cases[] = { + /* delay-seconds */ + { "0", NULL, RA_BASE, APR_SUCCESS, 0 }, + { "120", NULL, RA_BASE, APR_SUCCESS, 120 }, + { "007", NULL, RA_BASE, APR_SUCCESS, 7 }, + { " \t3\t ", NULL, RA_BASE, APR_SUCCESS, 3 }, + { "9223372036853", NULL, RA_BASE, APR_SUCCESS, APR_INT64_C(9223372036853) }, + { "9223372036855", NULL, RA_BASE, APR_SUCCESS, RA_MAX }, + { "99999999999999999999999999", NULL, RA_BASE, APR_SUCCESS, RA_MAX }, + /* HTTP-date, IMF-fixdate */ + { RA_DATE, NULL, RA_BASE - 60, APR_SUCCESS, 60 }, + { RA_DATE, NULL, RA_BASE, APR_SUCCESS, 0 }, + { RA_DATE, NULL, RA_BASE + 60, APR_SUCCESS, 0 }, + { RA_DATE " ", NULL, RA_BASE - 5, APR_SUCCESS, 5 }, + { "Thu, 01 Jan 1970 00:00:00 GMT", NULL, RA_BASE, APR_SUCCESS, 0 }, + { "Fri, 19 Jan 2038 03:14:07 GMT", NULL, 2147483647 - 9, APR_SUCCESS, 9 }, + { "Sat, 29 Feb 2020 12:00:00 GMT", NULL, APR_INT64_C(1582977600) - 3, APR_SUCCESS, 3 }, + { "Sun, 06 Nov 1994 08:49:60 GMT", NULL, RA_BASE - 3, APR_SUCCESS, 26 }, + /* the day name is checked for its syntax only */ + { "Mon, 06 Nov 1994 08:49:37 GMT", NULL, RA_BASE - 5, APR_SUCCESS, 5 }, + /* HTTP-date, rfc850-date and asctime-date */ + { "Sunday, 06-Nov-94 08:49:37 GMT", NULL, RA_BASE - 5, APR_SUCCESS, 5 }, + { "Sun Nov 6 08:49:37 1994", NULL, RA_BASE - 5, APR_SUCCESS, 5 }, + { "Sun Nov 06 08:49:37 1994", NULL, RA_BASE - 5, APR_SUCCESS, 5 }, + { "Sun Nov 16 08:49:37 1994", NULL, RA_BASE - 5 + 10 * 86400, APR_SUCCESS, 5 }, + /* a two digit year is not more than 50 years in the future */ + { "Sunday, 06-Nov-44 08:49:37 GMT", NULL, RA_BASE, APR_SUCCESS, APR_INT64_C(1577923200) }, + { "Sunday, 06-Nov-45 08:49:37 GMT", NULL, RA_BASE, APR_SUCCESS, 0 }, + /* the Date of the response wins over the local clock */ + { RA_DATE, "Sun, 06 Nov 1994 08:49:07 GMT", RA_BASE + 3600, APR_SUCCESS, 30 }, + { RA_DATE, "Sun, 06 Nov 1994 08:49:07 GMT", RA_BASE - 3600, APR_SUCCESS, 30 }, + { RA_DATE, "Sun, 06 Nov 1994 08:50:07 GMT", RA_BASE - 3600, APR_SUCCESS, 0 }, + /* a Date which is not an HTTP-date is not used */ + { RA_DATE, "garbage", RA_BASE - 7, APR_SUCCESS, 7 }, + { RA_DATE, "Sun, 06 Nov 1994 08:49:07 PST", RA_BASE - 7, APR_SUCCESS, 7 }, + { RA_DATE, "", RA_BASE - 7, APR_SUCCESS, 7 }, + /* not delay-seconds */ + { "", NULL, RA_BASE, APR_EINVAL, 0 }, + { " \t", NULL, RA_BASE, APR_EINVAL, 0 }, + { "+5", NULL, RA_BASE, APR_EINVAL, 0 }, + { "-5", NULL, RA_BASE, APR_EINVAL, 0 }, + { "5.0", NULL, RA_BASE, APR_EINVAL, 0 }, + { "1e3", NULL, RA_BASE, APR_EINVAL, 0 }, + { "0x10", NULL, RA_BASE, APR_EINVAL, 0 }, + { "1 2", NULL, RA_BASE, APR_EINVAL, 0 }, + { "120, 60", NULL, RA_BASE, APR_EINVAL, 0 }, + { "120,", NULL, RA_BASE, APR_EINVAL, 0 }, + { "5 seconds", NULL, RA_BASE, APR_EINVAL, 0 }, + /* not an HTTP-date: anything around or after a valid date */ + { RA_DATE ", 120", NULL, RA_BASE, APR_EINVAL, 0 }, + { "120, " RA_DATE, NULL, RA_BASE, APR_EINVAL, 0 }, + { RA_DATE ", " RA_DATE, NULL, RA_BASE, APR_EINVAL, 0 }, + { RA_DATE "junk", NULL, RA_BASE, APR_EINVAL, 0 }, + { RA_DATE " 5", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37 GMT\r", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sunday, 06-Nov-94 08:49:37 GMT, 120", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun Nov 6 08:49:37 1994 junk", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun Nov 6 08:49:37 1994 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + /* not an HTTP-date: a time zone other than GMT */ + { "Sun, 06 Nov 1994 08:49:37 PST", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37 UTC", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37 +0100", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37 gmt", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sunday, 06-Nov-94 08:49:37 PST", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37", NULL, RA_BASE, APR_EINVAL, 0 }, + /* not an HTTP-date: the grammar is case sensitive and exact */ + { "Xyz, 06 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "sun, 06 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 6 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 94 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 8:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06\tNov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08-49-37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sunday, 06-Nov-1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06-Nov-94 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sunday, 06 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "sunday, 06-Nov-94 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun Nov 6 08:49:37 1994", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun Nov 6 08:49:37 1994", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun Nov 6 08:49:37 94", NULL, RA_BASE, APR_EINVAL, 0 }, + /* not an HTTP-date: values out of range */ + { "Sun, 00 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 32 Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 31 Apr 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Wed, 29 Feb 1995 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 29 Feb 1900 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Foo 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 24:00:00 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:60:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 08:49:61 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 0a Nov 1994 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 19x4 08:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "Sun, 06 Nov 1994 +8:49:37 GMT", NULL, RA_BASE, APR_EINVAL, 0 }, + { "not a date", NULL, RA_BASE, APR_EINVAL, 0 }, + { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + NULL, RA_BASE, APR_EINVAL, 0 }, +}; + +const size_t ap_parse_retry_after_cases_len = + sizeof(ap_parse_retry_after_cases) / + sizeof(ap_parse_retry_after_cases[0]); + +HTTPD_START_LOOP_TEST(parse_retry_after_handles_seconds_and_dates, ap_parse_retry_after_cases_len) +{ + const struct ap_parse_retry_after_case *c = &ap_parse_retry_after_cases[_i]; + apr_interval_time_t delay = -1; + apr_status_t rv; + + rv = ap_parse_retry_after(&delay, c->input, c->date, + apr_time_from_sec(c->now)); + ck_assert_int_eq(rv, c->expected_rv); + if (rv == APR_SUCCESS) { + ck_assert(delay == (c->expected == RA_MAX ? APR_INT64_MAX + : apr_time_from_sec(c->expected))); + } +} +END_TEST + +START_TEST(parse_retry_after_rejects_null) +{ + apr_interval_time_t delay = 0; + + ck_assert_int_eq(ap_parse_retry_after(&delay, NULL, NULL, 0), APR_EINVAL); +} +END_TEST + +/* The delay never wraps around, whatever the local clock says. */ +START_TEST(parse_retry_after_saturates_for_extreme_clocks) +{ + const char *far = "Fri, 31 Dec 9999 23:59:59 GMT"; + const char *old = "Sunday, 06-Nov-94 08:49:37 GMT"; + apr_interval_time_t delay; + + delay = 0; + ck_assert_int_eq(ap_parse_retry_after(&delay, far, NULL, APR_INT64_MIN), + APR_SUCCESS); + ck_assert(delay == APR_INT64_MAX); + + delay = 0; + ck_assert_int_eq(ap_parse_retry_after(&delay, far, NULL, + APR_INT64_MIN + APR_INT64_C(1000000000000)), + APR_SUCCESS); + ck_assert(delay == APR_INT64_MAX); + + delay = 0; + ck_assert_int_eq(ap_parse_retry_after(&delay, far, NULL, -1), APR_SUCCESS); + ck_assert(delay == APR_INT64_C(253402300799) * APR_USEC_PER_SEC + 1); + + /* a two digit year is resolved against any clock, too */ + delay = 0; + ck_assert_int_eq(ap_parse_retry_after(&delay, old, NULL, APR_INT64_MIN), + APR_SUCCESS); + ck_assert(delay == APR_INT64_MAX); + + delay = -1; + ck_assert_int_eq(ap_parse_retry_after(&delay, old, NULL, APR_INT64_MAX), + APR_SUCCESS); + ck_assert(delay == 0); +} +END_TEST + + /* * Test Case Boilerplate */