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 */