diff options
author | Stefan Eissing <icing@apache.org> | 2019-10-16 14:31:43 +0200 |
---|---|---|
committer | Stefan Eissing <icing@apache.org> | 2019-10-16 14:31:43 +0200 |
commit | 77c96847ce410d47678f78cb543b1dae7db47112 (patch) | |
tree | 0e03267b354434394872c688d979b974393a7bc2 | |
parent | update mod_md tags (diff) | |
download | apache2-77c96847ce410d47678f78cb543b1dae7db47112.tar.xz apache2-77c96847ce410d47678f78cb543b1dae7db47112.zip |
*) mod_md: Adding the several new features.
The module offers an implementation of OCSP Stapling that can replace fully or
for a limited set of domains the existing one from mod_ssl. OCSP handling
is part of mod_md's monitoring and message notifications. If can be used
for sites that do not have ACME certificates.
The url for a CTLog Monitor can be configured. It is used in the server-status
to link to the external status page of a certicate.
The MDMessageCmd is called with argument "installed" when a new certificate
has been activated on server restart/reload. This allows for processing of
the new certificate, for example to applications that require it in different
locations or formats.
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1868506 13f79535-47bb-0310-9956-ffa450edef68
51 files changed, 4975 insertions, 1700 deletions
@@ -1,6 +1,20 @@ -*- coding: utf-8 -*- Changes with Apache 2.5.1 + *) mod_md: Adding the several new features. + The module offers an implementation of OCSP Stapling that can replace fully or + for a limited set of domains the existing one from mod_ssl. OCSP handling + is part of mod_md's monitoring and message notifications. If can be used + for sites that do not have ACME certificates. + The url for a CTLog Monitor can be configured. It is used in the server-status + to link to the external status page of a certicate. + The MDMessageCmd is called with argument "installed" when a new certificate + has been activated on server restart/reload. This allows for processing of + the new certificate, for example to applications that require it in different + locations or formats. + [Stefan Eissing] + + *) mod_deflate, mod_brotli: honor "Accept-Encoding: foo;q=0" as per RFC 7231; which means 'foo' is "not acceptable". PR 58158 [Chistophe Jaillet] diff --git a/CMakeLists.txt b/CMakeLists.txt index 546afe1a67..1c5621fb4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -517,9 +517,10 @@ SET(mod_md_extra_sources modules/md/md_result.c modules/md/md_reg.c modules/md/md_status.c modules/md/md_store.c modules/md/md_store_fs.c modules/md/md_time.c - modules/md/md_util.c + modules/md/md_ocsp.c modules/md/md_util.c modules/md/mod_md_config.c modules/md/mod_md_drive.c modules/md/mod_md_os.c modules/md/mod_md_status.c + modules/md/mod_md_ocsp.c ) SET(mod_optional_hook_export_extra_defines AP_DECLARE_EXPORT) # bogus reuse of core API prefix SET(mod_proxy_extra_defines PROXY_DECLARE_EXPORT) diff --git a/build/apr_common.m4 b/build/apr_common.m4 index f4e2dfd0a7..6b5c0f033b 100644 --- a/build/apr_common.m4 +++ b/build/apr_common.m4 @@ -511,9 +511,9 @@ AC_DEFUN([APR_TRY_COMPILE_NO_WARNING], [int main(int argc, const char *const *argv) {] [[$2]] [ return 0; }] - )], [CFLAGS=$apr_save_CFLAGS -$3], [CFLAGS=$apr_save_CFLAGS -$4]) + )], + [$3], [$4]) + CFLAGS=$apr_save_CFLAGS ]) dnl @@ -975,44 +975,11 @@ AC_SUBST(MKDEP) ]) dnl -dnl APR_CHECK_TYPES_FMT_COMPATIBLE(TYPE-1, TYPE-2, FMT-TAG, -dnl [ACTION-IF-TRUE], [ACTION-IF-FALSE]) -dnl -dnl Try to determine whether two types are the same and accept the given -dnl printf formatter (bare token, e.g. literal d, ld, etc). -dnl -AC_DEFUN([APR_CHECK_TYPES_FMT_COMPATIBLE], [ -define([apr_cvname], apr_cv_typematch_[]translit([$1], [ ], [_])_[]translit([$2], [ ], [_])_[][$3]) -AC_CACHE_CHECK([whether $1 and $2 use fmt %$3], apr_cvname, [ -APR_TRY_COMPILE_NO_WARNING([#include <sys/types.h> -#include <stdio.h> -#ifdef HAVE_STDINT_H -#include <stdint.h> -#endif -], [ - $1 chk1, *ptr1; - $2 chk2, *ptr2 = &chk1; - ptr1 = &chk2; - *ptr1 = *ptr2 = 0; - printf("%$3 %$3", chk1, chk2); -], [apr_cvname=yes], [apr_cvname=no])]) -if test "$apr_cvname" = "yes"; then - : - $4 -else - : - $5 -fi -]) - -dnl dnl APR_CHECK_TYPES_COMPATIBLE(TYPE-1, TYPE-2, [ACTION-IF-TRUE]) dnl dnl Try to determine whether two types are the same. Only works dnl for gcc and icc. dnl -dnl @deprecated @see APR_CHECK_TYPES_FMT_COMPATIBLE -dnl AC_DEFUN([APR_CHECK_TYPES_COMPATIBLE], [ define([apr_cvname], apr_cv_typematch_[]translit([$1], [ ], [_])_[]translit([$2], [ ], [_])) AC_CACHE_CHECK([whether $1 and $2 are the same], apr_cvname, [ diff --git a/docs/manual/mod/mod_md.xml b/docs/manual/mod/mod_md.xml index 972e371d44..4f2fd499e2 100644 --- a/docs/manual/mod/mod_md.xml +++ b/docs/manual/mod/mod_md.xml @@ -33,16 +33,20 @@ <summary> <p> This module manages common properties of domains for one or more virtual hosts. - Its main feature is the use of the ACME protocol - (<a href="https://tools.ietf.org/html/rfc8555">RFC 8555</a>) - to automate certificate provisioning. Certificates will be renewed - by the module ahead of their expiration to account for disruption in internet - services. There are ways to monitor the status of all Managed Domains - and configurations that will run your own notification commands on renewal, - expiration and errors. - </p> - <p> - The default ACME Certificate Authority is + Its serves two main purposes: for one, supervise/renew https: certificates via the + ACME protocol (<a href="https://tools.ietf.org/html/rfc8555">RFC 8555</a>). + Certificates will be renewed by the module ahead of their expiration to account + for disruption in internet services. There are ways to monitor the status of all + certififcates managed this way and configurations that will run your own + notification commands on renewal, expiration and errors. + </p><p> + Second, mod_md offers an alternate OCSP Stapling implementation. This works with + managed certificates as well as with certificates you configure yourself. OCSP + Stapling is a necessary component for any https: site, influencing page load + times and, depending on other setups, page availability. More in the + stapling section below. + </p><p> + The default ACME Authority for managing certificates is <a href="https://letsencrypt.org/">Let's Encrypt</a>, but it is possible to configure another CA that supports the protocol. </p> @@ -234,6 +238,53 @@ MDChallengeDns01 /usr/bin/acme-setup-dns </p> </note> + <note><title>Stapling</title> + <p> + If you want to try the stapling in one Managed Domain alone at first, + configure: + </p> + <highlight language="config"> +<MDomain mydomain.net> + MDStapling on +</MDomain> + </highlight> + <p> + and use the 'server-status' and/or MDMessageCmd to see how it operates. You will + see if Stapling information is there, how long it is valid, from where it came and + when it will be refreshed. + </p><p> + If this all works to your satisfaction, you can switch it on for all your + certificates or just your managed ones. + </p><p> + The existing stapling implementation by mod_ssl is used by many sites + for years. There are two main differences between the mod_ssl and mod_md + one: + </p> + <ol> + <li>On demand vs. scheduled: mod_ssl retrieves the stapling information + when it is requested, e.g. on a new connection. mod_md retrieves it + right at server start and after 2/3rds of its lifetime.</li> + <li>In memory vs. persisted: mod_ssl <em>can</em> persist this + information, but most example configurations use a memory cache. mod_md + always stores in the file system.</li> + </ol> + <p> + If you are unlucky and restart your server during an outage of your CA's + OCSP service, your users may no longer reach your sites. Without persistence + your server cannot provide the client with the data and the client browser + cannot get it as well, since the OCSP service is not responding. + </p><p> + The implementation in mod_md will have peristed it, load it again after + restart and have it available for incoming connections. A day or two before + this information expires, it will renew it, making it able to copy with + a long OCSP service downtime. + </p><p> + Due to backward compatibility, the existing implementation in mod_ssl could + not be changed drastically. For example, mod_ssl is unable to add a dependency + to mod_watchdog without braking many existing installations (that do not load it). + </p> + </note> + </summary> <directivesynopsis> @@ -781,22 +832,24 @@ MDRequireHttps permanent </contextlist> <usage> <p> - Sets challenge types and their execution order when proving domain ownership - and overrides any guesswork and sanity checks by the module. - The names are protocol specific. - The current ACME protocol version implemented by Let's Encrypt defines three challenge - types that are supported by mod_md. By default, it will try - the https: based one on port 443 when available. + Sets challenge types (in order of preference) when proving domain ownership. + Supported by the module are the challenge methods 'tls-alpn-01', 'dns-01' + and 'http-01'. The module will look at the overall configuation of the server + to find out which methods can be used. + </p><p> + If the server listens on port 80, for example, the 'http-01' method is available. + The prerequisite for 'dns-01' is a configured 'MDChallengeDns01' command. + 'tls-alpn-01' is described above in 'https: Challenges'. </p><p> - To repeat: using this directive overrides the module selection. If you specify - the 'http-01' challenge, the module will no longer check if the server listens - on port 80. It will just use the challenge with Let's Encrypt (if LE offers - it). + This auto selection works for most setups. But since Apache is a very powerful + server with many configuration options, the situation is not clear for all + possible cases. For example: it may listen on multiple IP addresses where some + are reachable on `https:` and some not. </p><p> - If your configuration choices here are unworkable, LE will fail - your domain verification after a while and give up. This error will - be reported on your server-status and md-status. You will then have to figure - out why it did not work. + If you configure 'MDCAChallenges' directly, this auto selection is disabled. + Instead, the module will use the configured challenge list when talking to + the ACME server (a challenge type must be offered by the server as well). + This challenges are examined in the order specified. </p> </usage> </directivesynopsis> @@ -948,7 +1001,7 @@ MDRequireHttps permanent <usage> <p> This command gets called when one of the following events happen for - a Managed Domain: "renewed", "expiring", "errored". The command may + a Managed Domain: "renewed", "installed", "expiring", "errored". The command may be invoked for more than these in the future and ignore events it is not prepared to handle. </p><p> @@ -967,13 +1020,25 @@ MDMessageCmd /etc/apache/md-message return code other than 0 is regarded as an error. </p><p> 'errored' is no immediate cause for concern since renewal is attempted - early enough to allow the internet to come back. + early enough to allow the internet to come back. This is reported at most + once per hour. </p><p> 'expiring' should be taken serious. It is issued when the <directive module="mod_md">MDWarnWindow</directive> is reached. By default this is 10% of the certificate lifetime, so for Let's Encrypt this currently means 9 days before it expires. The warning is repeated at most once a day. + </p><p> + 'renewed' means that a new certificate has been obtained and is stored + in the 'staging' area in the MD store. It will be activated on the next + server restart/reload. + </p><p> + 'installed' is triggered when a new certificate has been transferred from + staging into the domains location in MD store. This happens at server + startup/reload. Different to all other invocations, MDMessageCmd is run + with root permissions (on *nix systems) and has access to the certificate + files (and keys). Certificates needed for other applications or + in different formats can be processed on this event. </p> </usage> </directivesynopsis> @@ -1021,5 +1086,134 @@ MDMessageCmd /etc/apache/md-message </usage> </directivesynopsis> + <directivesynopsis> + <name>MDCertificateMonitor</name> + <description>The URL of a certificate log monitor.</description> + <syntax>MDCertificateMonitor name url</syntax> + <default>crt.sh https://crt.sh?q=</default> + <contextlist> + <context>server config</context> + </contextlist> + <usage> + <p> + This is part of the 'server-status' HTML user interface and has nothing to + do with the core functioning itself. It defines the link offered on that + page for easy checking of a certificate monitor. The SHA256 fingerprint + of the certificate is appended to the configured url. + </p><p> + Certificate Monitors offer supervision of Certificate Transparency (CT) + Logs to track the use of certificates for domains. The least you may see + is that Let's Encrypt (or whichever CA you have configured) has entered + your certificates into the CTLogs. + </p><p> + Caveat: certificate logs update and monitor's intakes of those + updates suffer some delay. This varies between logs and monitors. A + brand new certificate will not be known immediately. + </p> + </usage> + </directivesynopsis> + + <directivesynopsis> + <name>MDStapling</name> + <description>Enable stapling for all or a particular MDomain.</description> + <syntax>MDStapling on|off</syntax> + <default>off</default> + <contextlist> + <context>server config</context> + </contextlist> + <usage> + <p> + mod_md offers an implementation for providing OCSP stapling information. + This is an alternative to the one provided by 'mod_ssl'. For backward + compatiblity, this is disabled by default. + </p><p> + The stapling can be switched on for all certificates on the server or + for an individual MDomain. This will replace any stapling configurtion + in `mod_ssl` for these hosts. When disabled, the 'mod_ssl' stapling + will do the work (if it is itself enabled, of course). This allows for + a gradual shift over from one implementation to the other. + </p><p> + The stapling of `mod_md` will also work for domains where the certificates + are not managed by this module (see MDStapleOthers for how to control this). + This allows use of the new stapling without using any ACME certificate + management. + </p> + </usage> + </directivesynopsis> + + <directivesynopsis> + <name>MDStapleOthers</name> + <description>Enable stapling for certificates not managed by mod_md.</description> + <syntax>MDStaplingOthers on|off</syntax> + <default>on</default> + <contextlist> + <context>server config</context> + </contextlist> + <usage> + <p> + This setting only takes effect when `MDStapling` is enabled. It controls + if `mod_md` should also provide stapling information for certificates + that are not directly controlled by it, e.g. renewed via an ACME CA. + </p> + </usage> + </directivesynopsis> + + <directivesynopsis> + <name>MDStaplingKeepResponse</name> + <description>Controls when old responses should be removed.</description> + <syntax>MDStaplingKeepResponse duration</syntax> + <default>7d</default> + <contextlist> + <context>server config</context> + </contextlist> + <usage> + <p> + This time window specifies when OCSP response data used in stapling + shall be removed from the store again. Response information older than + 7 days (default) is deleted on server restart/reload. This keeps the store + from growing when certificates are renewed/reconfigured frequently. + </p><p> + </p> + </usage> + </directivesynopsis> + + <directivesynopsis> + <name>MDStaplingRenewWindow</name> + <description>Control when the stapling responses will be renewed.</description> + <syntax>MDStaplingRenewWindow duration</syntax> + <default>33%</default> + <contextlist> + <context>server config</context> + </contextlist> + <usage> + <p> + If the validity of the OCSP response used in stapling falls below 'duration', + mod_md will obtain a new OCSP response. + </p><p> + The CA issueing a certificate commonly also operates the OCSP responder + service and determines how long its signed response about the validity + of a certificate are itself valid. The longer a response is valid, the longer + it can be cached which mean better overall performance for everyone. + The shorter the life time, the more rapidly certificate revocations + spread to clients. Also, service reliability is a consideration. + </p><p> + By adjusting the stapling renew window you can control parts of this yourself. + If you make the renew time short (e.g. a short time before the current + information expires), you gain maximum cache time. But a service outage + (down for maintenance, for example) will affect you. If you renew a long + time before expiry, updates will be made more frequent, cause more load + on the CA server infrastructure and also more coordination between + the child processes of your server. + </p><p> + The default is chosen as 33%, which means renewal is started when only + a third of the response lifetime is left. For a CA that issues OCSP + responses with lifetime of 3 days, this means 2 days of caching and 1 day + for renewal attempts. A service outage would have to last full 24 hours + to affect your domains. + </p><p> + Setting an absolute renew window, like `2d` (2 days), is also possible. + </p> + </usage> + </directivesynopsis> </modulesynopsis> diff --git a/modules/md/config2.m4 b/modules/md/config2.m4 index 0d8d678780..14898c51df 100644 --- a/modules/md/config2.m4 +++ b/modules/md/config2.m4 @@ -150,6 +150,7 @@ md_http.lo dnl md_json.lo dnl md_jws.lo dnl md_log.lo dnl +md_ocsp.lo dnl md_result.lo dnl md_reg.lo dnl md_status.lo dnl @@ -160,6 +161,7 @@ md_util.lo dnl mod_md.lo dnl mod_md_config.lo dnl mod_md_drive.lo dnl +mod_md_ocsp.lo dnl mod_md_os.lo dnl mod_md_status.lo dnl " diff --git a/modules/md/md.h b/modules/md/md.h index f617dd31db..182d00b484 100644 --- a/modules/md/md.h +++ b/modules/md/md.h @@ -24,7 +24,9 @@ struct apr_array_header_t; struct apr_hash_t; struct md_json_t; struct md_cert_t; +struct md_job_t; struct md_pkey_t; +struct md_result_t; struct md_store_t; struct md_srv_conf_t; struct md_pkey_spec_t; @@ -41,6 +43,9 @@ struct md_pkey_spec_t; #define MD_TIME_LIFE_NORM (apr_time_from_sec(100 * MD_SECS_PER_DAY)) #define MD_TIME_RENEW_WINDOW_DEF (apr_time_from_sec(33 * MD_SECS_PER_DAY)) #define MD_TIME_WARN_WINDOW_DEF (apr_time_from_sec(10 * MD_SECS_PER_DAY)) +#define MD_TIME_OCSP_KEEP_NORM (apr_time_from_sec(7 * MD_SECS_PER_DAY)) + +#define MD_OTHER "other" typedef enum { MD_S_UNKNOWN = 0, /* MD has not been analysed yet */ @@ -59,25 +64,6 @@ typedef enum { } md_require_t; typedef enum { - MD_SV_TEXT, - MD_SV_JSON, - MD_SV_CERT, - MD_SV_PKEY, - MD_SV_CHAIN, -} md_store_vtype_t; - -typedef enum { - MD_SG_NONE, - MD_SG_ACCOUNTS, - MD_SG_CHALLENGES, - MD_SG_DOMAINS, - MD_SG_STAGING, - MD_SG_ARCHIVE, - MD_SG_TMP, - MD_SG_COUNT, -} md_store_group_t; - -typedef enum { MD_RENEW_DEFAULT = -1, /* default value */ MD_RENEW_MANUAL, /* manually triggered renewal of certificate */ MD_RENEW_AUTO, /* automatic process performed by httpd */ @@ -96,8 +82,8 @@ struct md_t { int renew_mode; /* mode of obtaining credentials */ struct md_pkey_spec_t *pkey_spec;/* specification for generating new private keys */ int must_staple; /* certificates should set the OCSP Must Staple extension */ - const md_timeslice_t *renew_window; /* time before expiration that starts renewal */ - const md_timeslice_t *warn_window; /* time before expiration that warnings are sent out */ + md_timeslice_t *renew_window; /* time before expiration that starts renewal */ + md_timeslice_t *warn_window; /* time before expiration that warnings are sent out */ const char *ca_url; /* url of CA certificate service */ const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ @@ -110,7 +96,9 @@ struct md_t { md_state_t state; /* state of this MD */ struct apr_array_header_t *acme_tls_1_domains; /* domains supporting "acme-tls/1" protocol */ + int stapling; /* if OCSP stapling is enabled */ + int watched; /* if certificate is supervised (renew or expiration warning) */ const struct md_srv_conf_t *sc; /* server config where it was defined or NULL */ const char *defn_name; /* config file this MD was defined */ unsigned defn_line_number; /* line number of definition */ @@ -120,6 +108,7 @@ struct md_t { #define MD_KEY_ACCOUNT "account" #define MD_KEY_ACME_TLS_1 "acme-tls/1" +#define MD_KEY_ACTIVATION_DELAY "activation-delay" #define MD_KEY_ACTIVITY "activity" #define MD_KEY_AGREEMENT "agreement" #define MD_KEY_AUTHORIZATIONS "authorizations" @@ -143,10 +132,13 @@ struct md_t { #define MD_KEY_DOMAINS "domains" #define MD_KEY_ENTRIES "entries" #define MD_KEY_ERRORED "errored" +#define MD_KEY_ERROR "error" #define MD_KEY_ERRORS "errors" #define MD_KEY_EXPIRES "expires" #define MD_KEY_FINALIZE "finalize" #define MD_KEY_FINISHED "finished" +#define MD_KEY_FROM "from" +#define MD_KEY_GOOD "good" #define MD_KEY_HTTP "http" #define MD_KEY_HTTPS "https" #define MD_KEY_ID "id" @@ -163,6 +155,8 @@ struct md_t { #define MD_KEY_NAME "name" #define MD_KEY_NEXT_RUN "next-run" #define MD_KEY_NOTIFIED "notified" +#define MD_KEY_OCSP "ocsp" +#define MD_KEY_OCSPS "ocsps" #define MD_KEY_ORDERS "orders" #define MD_KEY_PERMANENT "permanent" #define MD_KEY_PKEY "privkey" @@ -172,41 +166,39 @@ struct md_t { #define MD_KEY_READY "ready" #define MD_KEY_REGISTRATION "registration" #define MD_KEY_RENEW "renew" +#define MD_KEY_RENEW_AT "renew-at" #define MD_KEY_RENEW_MODE "renew-mode" #define MD_KEY_RENEWAL "renewal" #define MD_KEY_RENEWING "renewing" #define MD_KEY_RENEW_WINDOW "renew-window" #define MD_KEY_REQUIRE_HTTPS "require-https" #define MD_KEY_RESOURCE "resource" +#define MD_KEY_RESPONSE "response" +#define MD_KEY_REVOKED "revoked" #define MD_KEY_SERIAL "serial" #define MD_KEY_SHA256_FINGERPRINT "sha256-fingerprint" +#define MD_KEY_STAPLING "stapling" #define MD_KEY_STATE "state" #define MD_KEY_STATUS "status" #define MD_KEY_STORE "store" +#define MD_KEY_SUBPROBLEMS "subproblems" #define MD_KEY_TEMPORARY "temporary" #define MD_KEY_TOKEN "token" #define MD_KEY_TOTAL "total" #define MD_KEY_TRANSITIVE "transitive" #define MD_KEY_TYPE "type" +#define MD_KEY_UNKNOWN "unknown" +#define MD_KEY_UNTIL "until" #define MD_KEY_URL "url" #define MD_KEY_URI "uri" +#define MD_KEY_VALID "valid" #define MD_KEY_VALID_FROM "valid-from" -#define MD_KEY_VALID_UNTIL "valid-until" #define MD_KEY_VALUE "value" #define MD_KEY_VERSION "version" +#define MD_KEY_WATCHED "watched" #define MD_KEY_WHEN "when" #define MD_KEY_WARN_WINDOW "warn-window" -#define MD_FN_MD "md.json" -#define MD_FN_JOB "job.json" -#define MD_FN_PRIVKEY "privkey.pem" -#define MD_FN_PUBCERT "pubcert.pem" -#define MD_FN_CERT "cert.pem" -#define MD_FN_HTTPD_JSON "httpd.json" - -#define MD_FN_FALLBACK_PKEY "fallback-privkey.pem" -#define MD_FN_FALLBACK_CERT "fallback-cert.pem" - /* Check if a string member of a new MD (n) has * a value and if it differs from the old MD o */ @@ -260,12 +252,6 @@ md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain); md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md); /** - * Find the managed domain in the list that, for the given md, - * has the same name, or the most number of overlaps in domains - */ -md_t *md_find_closest_match(struct apr_array_header_t *mds, const md_t *md); - -/** * Create and empty md record, structures initialized. */ md_t *md_create_empty(apr_pool_t *p); @@ -303,6 +289,12 @@ int md_is_covered_by_alt_names(const md_t *md, const struct apr_array_header_t* /**************************************************************************************************/ +/* notifications */ + +typedef apr_status_t md_job_notify_cb(struct md_job_t *job, const char *reason, + struct md_result_t *result, apr_pool_t *p, void *baton); + +/**************************************************************************************************/ /* domain credentials */ typedef struct md_pubcert_t md_pubcert_t; diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c index d2cc00a10e..d42ea72230 100644 --- a/modules/md/md_acme.c +++ b/modules/md/md_acme.c @@ -99,16 +99,16 @@ static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs) } } -static apr_status_t http_update_nonce(const md_http_response_t *res) +static apr_status_t http_update_nonce(const md_http_response_t *res, void *data) { + md_acme_t *acme = data; if (res->headers) { const char *nonce = apr_table_get(res->headers, "Replay-Nonce"); if (nonce) { - md_acme_t *acme = res->req->baton; acme->nonce = apr_pstrdup(acme->p, nonce); } } - return res->rv; + return APR_SUCCESS; } static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, const char *url) @@ -144,12 +144,12 @@ static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, co static apr_status_t acmev1_new_nonce(md_acme_t *acme) { - return md_http_HEAD(acme->http, acme->api.v1.new_reg, NULL, http_update_nonce, acme); + return md_http_HEAD_perform(acme->http, acme->api.v1.new_reg, NULL, http_update_nonce, acme); } static apr_status_t acmev2_new_nonce(md_acme_t *acme) { - return md_http_HEAD(acme->http, acme->api.v2.new_nonce, NULL, http_update_nonce, acme); + return md_http_HEAD_perform(acme->http, acme->api.v2.new_nonce, NULL, http_update_nonce, acme); } @@ -175,7 +175,10 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t ptype = md_json_gets(problem, MD_KEY_TYPE, NULL); pdetail = md_json_gets(problem, MD_KEY_DETAIL, NULL); req->rv = problem_status_get(ptype); - md_result_problem_set(req->result, req->rv, ptype, pdetail); + md_result_problem_set(req->result, req->rv, ptype, pdetail, + md_json_getj(problem, MD_KEY_SUBPROBLEMS, NULL)); + + if (APR_STATUS_IS_EAGAIN(req->rv)) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, req->rv, req->p, @@ -189,23 +192,21 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t } } - if (APR_SUCCESS == res->rv) { - switch (res->status) { - case 400: - return APR_EINVAL; - case 403: - return APR_EACCES; - case 404: - return APR_ENOENT; - default: - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p, - "acme problem unknown: http status %d", res->status); - md_result_printf(req->result, APR_EGENERAL, "unexpected http status: %d", - res->status); - return req->result->status; - } + switch (res->status) { + case 400: + return APR_EINVAL; + case 403: + return APR_EACCES; + case 404: + return APR_ENOENT; + default: + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p, + "acme problem unknown: http status %d", res->status); + md_result_printf(req->result, APR_EGENERAL, "unexpected http status: %d", + res->status); + return req->result->status; } - return res->rv; + return APR_SUCCESS; } /**************************************************************************************************/ @@ -213,51 +214,49 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t static apr_status_t acmev1_req_init(md_acme_req_t *req, md_json_t *jpayload) { - const char *payload; - size_t payload_len; + md_data_t payload; if (!req->acme->acct) { return APR_EINVAL; } if (jpayload) { - payload = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); - if (!payload) { + payload.data = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); + if (!payload.data) { return APR_EINVAL; } } else { - payload = ""; + payload.data = ""; } - payload_len = strlen(payload); + payload.len = strlen(payload.data); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, - "acme payload(len=%" APR_SIZE_T_FMT "): %s", payload_len, payload); - return md_jws_sign(&req->req_json, req->p, payload, payload_len, + "acme payload(len=%" APR_SIZE_T_FMT "): %s", payload.len, payload.data); + return md_jws_sign(&req->req_json, req->p, &payload, req->prot_hdrs, req->acme->acct_key, NULL); } static apr_status_t acmev2_req_init(md_acme_req_t *req, md_json_t *jpayload) { - const char *payload; - size_t payload_len; + md_data_t payload; if (!req->acme->acct) { return APR_EINVAL; } if (jpayload) { - payload = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); - if (!payload) { + payload.data = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); + if (!payload.data) { return APR_EINVAL; } } else { - payload = ""; + payload.data = ""; } - payload_len = strlen(payload); + payload.len = strlen(payload.data); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, - "acme payload(len=%" APR_SIZE_T_FMT "): %s", payload_len, payload); - return md_jws_sign(&req->req_json, req->p, payload, payload_len, + "acme payload(len=%" APR_SIZE_T_FMT "): %s", payload.len, payload.data); + return md_jws_sign(&req->req_json, req->p, &payload, req->prot_hdrs, req->acme->acct_key, req->acme->acct->url); } @@ -284,14 +283,10 @@ static apr_status_t md_acme_req_done(md_acme_req_t *req, apr_status_t rv) return rv; } -static apr_status_t on_response(const md_http_response_t *res) +static apr_status_t on_response(const md_http_response_t *res, void *data) { - md_acme_req_t *req = res->req->baton; - apr_status_t rv = res->rv; - - if (APR_SUCCESS != rv) { - goto out; - } + md_acme_req_t *req = data; + apr_status_t rv = APR_SUCCESS; req->resp_hdrs = apr_table_clone(req->p, res->headers); req_update_nonce(req->acme, res->headers); @@ -340,7 +335,6 @@ static apr_status_t on_response(const md_http_response_t *res) return rv; } -out: md_acme_req_done(req, rv); return rv; } @@ -355,7 +349,7 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req) { apr_status_t rv; md_acme_t *acme = req->acme; - const char *body = NULL; + md_data_t *body = NULL; md_result_t *result; assert(acme->url); @@ -406,15 +400,17 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req) if (APR_SUCCESS != rv) goto leave; if (req->req_json) { - body = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT); + body = apr_pcalloc(req->p, sizeof(*body)); + body->data = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT); if (!body) { rv = APR_EINVAL; goto leave; } + body->len = strlen(body->data); } if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p, - "req: %s %s, body:\n%s", req->method, req->url, body); + "req: %s %s, body:\n%s", req->method, req->url, body->data); } else { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, @@ -422,14 +418,14 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req) } if (!strcmp("GET", req->method)) { - rv = md_http_GET(req->acme->http, req->url, NULL, on_response, req); + rv = md_http_GET_perform(req->acme->http, req->url, NULL, on_response, req); } else if (!strcmp("POST", req->method)) { - rv = md_http_POSTd(req->acme->http, req->url, NULL, "application/jose+json", - body, body? strlen(body) : 0, on_response, req); + rv = md_http_POSTd_perform(req->acme->http, req->url, NULL, "application/jose+json", + body, on_response, req); } else if (!strcmp("HEAD", req->method)) { - rv = md_http_HEAD(req->acme->http, req->url, NULL, on_response, req); + rv = md_http_HEAD_perform(req->acme->http, req->url, NULL, on_response, req); } else { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, req->p, @@ -501,7 +497,8 @@ void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t md_result_set(result, rv, NULL); } else { - md_result_problem_set(result, acme->last->status, acme->last->problem, acme->last->detail); + md_result_problem_set(result, acme->last->status, acme->last->problem, + acme->last->detail, acme->last->subproblems); } } @@ -658,7 +655,7 @@ apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url, acme->version = MD_ACME_VERSION_UNKNOWN; acme->last = md_result_make(acme->p, APR_SUCCESS); - *pacme = (APR_SUCCESS == rv)? acme : NULL; + *pacme = acme; return rv; } @@ -667,17 +664,16 @@ typedef struct { md_result_t *result; } update_dir_ctx; -static apr_status_t update_directory(const md_http_response_t *res) +static apr_status_t update_directory(const md_http_response_t *res, void *data) { md_http_request_t *req = res->req; - md_acme_t *acme = ((update_dir_ctx *)req->baton)->acme; - md_result_t *result = ((update_dir_ctx *)req->baton)->result; - apr_status_t rv = res->rv; + md_acme_t *acme = ((update_dir_ctx *)data)->acme; + md_result_t *result = ((update_dir_ctx *)data)->result; + apr_status_t rv; md_json_t *json; const char *s; - if (APR_SUCCESS != rv) goto leave; - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->pool, "directory lookup response: %d", res->status); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, "directory lookup response: %d", res->status); if (res->status == 503) { md_result_printf(result, APR_EAGAIN, "The ACME server at <%s> reports that Service is Unavailable (503). This " @@ -691,7 +687,8 @@ static apr_status_t update_directory(const md_http_response_t *res) "The ACME server at <%s> responded with HTTP status %d. This " "is unusual. Please verify that the URL is correct and that you can indeed " "make request from the server to it by other means, e.g. invoking curl/wget.", - acme->url, res->status); + acme->url, res->status); + rv = result->status; goto leave; } @@ -762,13 +759,18 @@ apr_status_t md_acme_setup(md_acme_t *acme, md_result_t *result) acme->user_agent, acme->proxy_url))) { return rv; } + /* TODO: maybe this should be configurable. Let's take some reasonable + * defaults for now that protect our client */ md_http_set_response_limit(acme->http, 1024*1024); + md_http_set_timeout_default(acme->http, apr_time_from_sec(10 * 60)); + md_http_set_connect_timeout_default(acme->http, apr_time_from_sec(30)); + md_http_set_stalling_default(acme->http, 10, apr_time_from_sec(30)); md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url); ctx.acme = acme; ctx.result = result; - rv = md_http_GET(acme->http, acme->url, NULL, update_directory, &ctx); + rv = md_http_GET_perform(acme->http, acme->url, NULL, update_directory, &ctx); if (APR_SUCCESS != rv && APR_SUCCESS == result->status) { /* If the result reports no error, we never got a response from the server */ diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c index e1e36dfff8..98443d2fd8 100644 --- a/modules/md/md_acme_acct.c +++ b/modules/md/md_acme_acct.c @@ -107,19 +107,13 @@ md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) s = NULL; break; } - if (s) { - md_json_sets(s, jacct, MD_KEY_STATUS, NULL); - } - md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); - md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); - md_json_setsa(acct->contacts, jacct, MD_KEY_CONTACT, NULL); - md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); - if (acct->agreement) { - md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); - } - if (acct->orders) { - md_json_sets(acct->orders, jacct, MD_KEY_ORDERS, NULL); - } + if (s) md_json_sets(s, jacct, MD_KEY_STATUS, NULL); + if (acct->url) md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); + if (acct->ca_url) md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); + if (acct->contacts) md_json_setsa(acct->contacts, jacct, MD_KEY_CONTACT, NULL); + if (acct->registration) md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); + if (acct->agreement) md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); + if (acct->orders) md_json_sets(acct->orders, jacct, MD_KEY_ORDERS, NULL); return jacct; } diff --git a/modules/md/md_acme_acct.h b/modules/md/md_acme_acct.h index 2b552add75..c2cf64c7e4 100644 --- a/modules/md/md_acme_acct.h +++ b/modules/md/md_acme_acct.h @@ -21,6 +21,7 @@ struct md_acme_req; struct md_json_t; struct md_pkey_t; +#include "md_store.h" /** * An ACME account at an ACME server. @@ -71,7 +72,7 @@ apr_status_t md_acme_acct_update(md_acme_t *acme); /** * Update the account and persist changes in the store, if given (and not NULL). */ -apr_status_t md_acme_acct_validate(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); +apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p); /** * Agree to the given Terms-of-Service url for the current account. @@ -103,23 +104,23 @@ const char *md_acme_get_agreement(md_acme_t *acme); * Find an existing account in the local store. On APR_SUCCESS, the acme * instance will have a current, validated account to use. */ -apr_status_t md_acme_find_acct(md_acme_t *acme, struct md_store_t *store); +apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store); /** * Find the account id for a given account url. */ -apr_status_t md_acme_acct_id_for_url(const char **pid, struct md_store_t *store, +apr_status_t md_acme_acct_id_for_url(const char **pid, md_store_t *store, md_store_group_t group, const char *url, apr_pool_t *p); /** * Create a new account at the ACME server. The * new account is the one used by the acme instance afterwards, on success. */ -apr_status_t md_acme_acct_register(md_acme_t *acme, struct md_store_t *store, +apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_t *p, apr_array_header_t *contacts, const char *agreement); -apr_status_t md_acme_acct_save(struct md_store_t *store, apr_pool_t *p, md_acme_t *acme, +apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, const char **pid, struct md_acme_acct_t *acct, struct md_pkey_t *acct_key); @@ -129,7 +130,7 @@ apr_status_t md_acme_acct_save(struct md_store_t *store, apr_pool_t *p, md_acme_ apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p); apr_status_t md_acme_acct_load(struct md_acme_acct_t **pacct, struct md_pkey_t **ppkey, - struct md_store_t *store, md_store_group_t group, + md_store_t *store, md_store_group_t group, const char *name, apr_pool_t *p); #endif /* md_acme_acct_h */ diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c index ddb4e91973..66aa865b5b 100644 --- a/modules/md/md_acme_authz.c +++ b/modules/md/md_acme_authz.c @@ -144,12 +144,31 @@ apr_status_t md_acme_authz_retrieve(md_acme_t *acme, apr_pool_t *p, const char * return rv; } +typedef struct { + apr_pool_t *p; + md_acme_authz_t *authz; +} error_ctx_t; + +static int copy_challenge_error(void *baton, size_t index, md_json_t *json) +{ + error_ctx_t *ctx = baton; + + (void)index; + if (md_json_has_key(json, MD_KEY_ERROR, NULL)) { + ctx->authz->error_type = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_TYPE, NULL); + ctx->authz->error_detail = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_DETAIL, NULL); + ctx->authz->error_subproblems = md_json_dupj(ctx->p, json, MD_KEY_ERROR, MD_KEY_SUBPROBLEMS, NULL); + } + return 1; +} + apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_pool_t *p) { md_json_t *json; const char *s, *err; md_log_level_t log_level; apr_status_t rv; + error_ctx_t ctx; assert(acme); assert(acme->http); @@ -158,6 +177,8 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p authz->state = MD_ACME_AUTHZ_S_UNKNOWN; json = NULL; + authz->error_type = authz->error_detail = NULL; + authz->error_subproblems = NULL; err = "unable to parse response"; log_level = MD_LOG_ERR; @@ -177,7 +198,10 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p log_level = MD_LOG_DEBUG; } else if (!strcmp(s, "invalid")) { + ctx.p = p; + ctx.authz = authz; authz->state = MD_ACME_AUTHZ_S_INVALID; + md_json_itera(copy_challenge_error, &ctx, json, MD_KEY_CHALLENGES, NULL); err = "challenge 'invalid'"; } } @@ -189,7 +213,7 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p if (md_log_is_level(p, log_level)) { md_log_perror(MD_LOG_MARK, log_level, rv, p, "ACME server authz: %s for %s at %s. " - "Exact response was: %s", err? err : "", authz->domain, authz->url, + "Exact response was: %s", err, authz->domain, authz->url, json? md_json_writep(json, p, MD_JSON_FMT_COMPACT) : "not available"); } @@ -243,7 +267,7 @@ static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_tab (void)p; (void)hdrs; (void)body; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "updated authz %s", ctx->authz->url); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ctx->p, "updated authz %s", ctx->authz->url); return APR_SUCCESS; } @@ -323,7 +347,7 @@ static apr_status_t cha_tls_alpn_01_setup(md_acme_authz_cha_t *cha, md_acme_auth const char *acme_id, *token; apr_status_t rv; int notify_server; - md_data data; + md_data_t data; (void)env; if (md_array_str_index(acme_tls_1_domains, authz->domain, 0, 0) < 0) { @@ -400,7 +424,7 @@ static apr_status_t cha_dns_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t * apr_status_t rv; int exit_code, notify_server; authz_req_ctx ctx; - md_data data; + md_data_t data; (void)store; (void)key_spec; @@ -582,7 +606,7 @@ apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, md_acme_t *acme, md_s rv = CHA_TYPES[i].setup(fctx.accepted, authz, acme, store, key_spec, acme_tls_1_domains, env, p); if (APR_SUCCESS == rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: set up challenge '%s'", authz->domain, fctx.accepted->type); challenge_setup = CHA_TYPES[i].name; diff --git a/modules/md/md_acme_authz.h b/modules/md/md_acme_authz.h index 4a3b453b29..fe1abe5528 100644 --- a/modules/md/md_acme_authz.h +++ b/modules/md/md_acme_authz.h @@ -49,6 +49,9 @@ struct md_acme_authz_t { const char *url; md_acme_authz_state_t state; apr_time_t expires; + const char *error_type; + const char *error_detail; + const struct md_json_t *error_subproblems; struct md_json_t *resource; }; diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index 4b29e4b044..b9c0c6d185 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -94,7 +94,7 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result) md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-using staged account"); } else if (!APR_STATUS_IS_ENOENT(rv)) { - goto out; + goto leave; } /* Get an account for the ACME server for this MD */ @@ -107,7 +107,7 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result) update_md = 1; } else if (APR_SUCCESS != rv) { - goto out; + goto leave; } } @@ -130,10 +130,11 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result) d->proto->protocol); if (!ad->md->contacts || apr_is_empty_array(md->contacts)) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, - "no contact information for md %s", md->name); rv = APR_EINVAL; - goto out; + md_result_printf(result, rv, "No contact information is available for MD %s. " + "Configure one using the ServerAdmin directive.", md->name); + md_result_log(result, MD_LOG_ERR); + goto leave; } /* ACMEv1 allowed registration of accounts without accepted Terms-of-Service. @@ -150,18 +151,24 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result) ad->acme->ca_agreement); md_result_log(result, MD_LOG_ERR); rv = result->status; - goto out; + goto leave; } rv = md_acme_acct_register(ad->acme, d->store, d->p, md->contacts, md->ca_agreement); - if (APR_SUCCESS == rv) { - md->ca_account = NULL; - update_md = 1; - update_acct = 1; + if (APR_SUCCESS != rv) { + if (APR_SUCCESS != ad->acme->last->status) { + md_result_dup(result, ad->acme->last); + md_result_log(result, MD_LOG_ERR); + } + goto leave; } + + md->ca_account = NULL; + update_md = 1; + update_acct = 1; } -out: +leave: /* Persist MD changes in STAGING, so we pick them up on next run */ if (APR_SUCCESS == rv&& update_md) { rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); @@ -255,7 +262,7 @@ apr_status_t md_acme_drive_cert_poll(md_proto_driver_t *d, int only_once) rv = md_util_try(get_cert, d, 1, ad->cert_poll_timeout, 0, 0, 1); } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "poll for cert at %s", ad->order->certificate); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "poll for cert at %s", ad->order->certificate); return rv; } @@ -477,11 +484,9 @@ out: /**************************************************************************************************/ /* ACME driver init */ -static apr_status_t acme_driver_init(md_proto_driver_t *d, md_result_t *result) +static apr_status_t acme_driver_preload_init(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad; - int dis_http, dis_https, dis_alpn_acme, dis_dns; - const char *challenge; md_result_set(result, APR_SUCCESS, NULL); @@ -495,6 +500,23 @@ static apr_status_t acme_driver_init(md_proto_driver_t *d, md_result_t *result) ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char*)); ad->certs = apr_array_make(d->p, 5, sizeof(md_cert_t*)); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, + "%s: init_base driver", d->md->name); + return result->status; +} + +static apr_status_t acme_driver_init(md_proto_driver_t *d, md_result_t *result) +{ + md_acme_driver_t *ad; + int dis_http, dis_https, dis_alpn_acme, dis_dns; + const char *challenge; + + acme_driver_preload_init(d, result); + md_result_set(result, APR_SUCCESS, NULL); + if (APR_SUCCESS != result->status) goto leave; + + ad = d->baton; + /* We can only support challenges if the server is reachable from the outside * via port 80 and/or 443. These ports might be mapped for httpd to something * else, but a mapping needs to exist. */ @@ -511,49 +533,49 @@ static apr_status_t acme_driver_init(md_proto_driver_t *d, md_result_t *result) APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_HTTP01; APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_TLSALPN01; APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_DNS01; - } - - if (!d->can_http && !d->can_https - && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 0) < 0) { - md_result_printf(result, APR_EGENERAL, - "the server seems neither reachable via http (port 80) nor https (port 443). " - "Please look at the MDPortMap configuration directive on how to correct this. " - "The ACME protocol needs at least one of those so the CA can talk to the server " - "and verify a domain ownership. Alternatively, you may configure support " - "for the %s challenge directive.", MD_AUTHZ_TYPE_DNS01); - goto leave; - } - - dis_http = dis_https = dis_alpn_acme = dis_dns = 0; - if (!d->can_http && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0, 1) >= 0) { - ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0); - dis_http = 1; - } - if (!d->can_https && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { - ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); - dis_https = 1; - } - if (apr_is_empty_array(d->md->acme_tls_1_domains) - && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { - ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); - dis_alpn_acme = 1; - } - if (!apr_table_get(d->env, MD_KEY_CMD_DNS01) && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 1) >= 0) { - ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0); - dis_dns = 1; - } - if (apr_is_empty_array(ad->ca_challenges)) { - md_result_printf(result, APR_EGENERAL, - "None of the ACME challenge methods configured for this domain are suitable.%s%s%s%s", - dis_http? " The http: challenge 'http-01' is disabled because the server seems not reachable on port 80." : "", - dis_https? " The https: challenge 'tls-alpn-01' is disabled because the server seems not reachable on port 443." : "", - dis_alpn_acme? "The https: challenge 'tls-alpn-01' is disabled because the Protocols configuration does not include the 'acme-tls/1' protocol." : "", - dis_dns? "The DNS challenge 'dns-01' is disabled because the directive 'MDChallengeDns01' is not configured." : "" - ); - goto leave; - } + if (!d->can_http && !d->can_https + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 0) < 0) { + md_result_printf(result, APR_EGENERAL, + "the server seems neither reachable via http (port 80) nor https (port 443). " + "Please look at the MDPortMap configuration directive on how to correct this. " + "The ACME protocol needs at least one of those so the CA can talk to the server " + "and verify a domain ownership. Alternatively, you may configure support " + "for the %s challenge directive.", MD_AUTHZ_TYPE_DNS01); + goto leave; + } + + dis_http = dis_https = dis_alpn_acme = dis_dns = 0; + if (!d->can_http && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0); + dis_http = 1; + } + if (!d->can_https && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); + dis_https = 1; + } + if (apr_is_empty_array(d->md->acme_tls_1_domains) + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); + dis_alpn_acme = 1; + } + if (!apr_table_get(d->env, MD_KEY_CMD_DNS01) && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0); + dis_dns = 1; + } + if (apr_is_empty_array(ad->ca_challenges)) { + md_result_printf(result, APR_EGENERAL, + "None of the ACME challenge methods configured for this domain are suitable.%s%s%s%s", + dis_http? " The http: challenge 'http-01' is disabled because the server seems not reachable on public port 80." : "", + dis_https? " The https: challenge 'tls-alpn-01' is disabled because the server seems not reachable on public port 443." : "", + dis_alpn_acme? " The https: challenge 'tls-alpn-01' is disabled because the Protocols configuration does not include the 'acme-tls/1' protocol." : "", + dis_dns? "The DNS challenge 'dns-01' is disabled because the directive 'MDChallengeDns01' is not configured." : "" + ); + goto leave; + } + } + leave: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, "%s: init driver", d->md->name); return result->status; @@ -570,11 +592,11 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) apr_time_t now; apr_array_header_t *staged_certs; char ts[APR_RFC822_DATE_LEN]; - + int first = 0; + if (md_log_is_level(d->p, MD_LOG_DEBUG)) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " - "state=%d, can_http=%d, can_https=%d, challenges='%s'", - d->md->name, d->md->state, d->can_http, d->can_https, + "state=%d, challenges='%s'", d->md->name, d->md->state, apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); } @@ -623,8 +645,15 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) rv = md_reg_get_cred_files(&keyfile, &certfile, d->reg, MD_SG_STAGING, d->md, d->p); if (APR_SUCCESS == rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: all data staged", d->md->name); - goto ready; + if (md_array_is_empty(ad->certs) + && APR_SUCCESS == md_pubcert_load(d->store, MD_SG_STAGING, d->md->name, &staged_certs, d->p)) { + apr_array_cat(ad->certs, staged_certs); + } + if (!md_array_is_empty(ad->certs)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: all data staged", d->md->name); + rv = APR_SUCCESS; + goto ready; + } } } @@ -644,7 +673,7 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) if (!ad->md || strcmp(ad->md->ca_url, d->md->ca_url)) { md_result_activity_printf(result, "Resetting staging for %s", d->md->name); /* re-initialize staging */ - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: setup staging", d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name); md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); ad->md = md_copy(d->p, d->md); ad->order = NULL; @@ -687,11 +716,11 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) if (md_array_is_empty(ad->certs) || ad->next_up_link) { md_result_activity_printf(result, "Retrieving certificate chain for %s", d->md->name); - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: retrieving certificate chain", d->md->name); rv = ad_chain_retrieve(d); if (APR_SUCCESS != rv) { - md_result_printf(result, rv, "Unable to retrive certificate chain."); + md_result_printf(result, rv, "Unable to retrieve certificate chain."); goto out; } @@ -707,13 +736,19 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) /* As last step, cleanup any order we created so that challenge data * may be removed asap. */ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md->name, d->env); - + + /* first time this job ran through */ + first = 1; ready: md_result_activity_setn(result, NULL); /* we should have the complete cert chain now */ assert(!md_array_is_empty(ad->certs)); assert(ad->certs->nelts > 1); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: certificate ready, activation delay set to %s", + d->md->name, md_duration_format(d->p, d->activation_delay)); + /* determine when it should be activated */ md_result_delay_set(result, md_cert_get_not_before(APR_ARRAY_IDX(ad->certs, 0, md_cert_t*))); @@ -725,10 +760,20 @@ ready: const md_pubcert_t *pub; apr_time_t valid_until, delay_activation; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: state is COMPLETE, checking existing certificate", d->md->name); if (APR_SUCCESS == md_reg_get_pubcert(&pub, d->reg, d->md, d->p)) { valid_until = md_cert_get_not_after(APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*)); - if (valid_until > now) { - delay_activation = apr_time_from_sec(MD_SECS_PER_DAY); + if (d->activation_delay < 0) { + /* special simulation for test case */ + if (first) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s: delay ready_at to now+1s", d->md->name); + md_result_delay_set(result, apr_time_now() + apr_time_from_sec(1)); + } + } + else if (valid_until > now) { + delay_activation = d->activation_delay; if (delay_activation > (valid_until - now)) { delay_activation = (valid_until - now); } @@ -742,13 +787,12 @@ ready: if (result->ready_at > now) { md_result_printf(result, APR_SUCCESS, "The certificate for the managed domain has been renewed successfully and can " - "be used from %s on. A graceful server restart in %s is recommended.", - ts, md_duration_print(d->p, result->ready_at - now)); + "be used from %s on.", ts); } else { md_result_printf(result, APR_SUCCESS, "The certificate for the managed domain has been renewed successfully and can " - "be used. A graceful server restart now is recommended."); + "be used (valid since %s). A graceful server restart now is recommended.", ts); } out: @@ -884,7 +928,8 @@ static apr_status_t acme_driver_preload(md_proto_driver_t *d, } static md_proto_t ACME_PROTO = { - MD_PROTO_ACME, acme_driver_init, acme_driver_renew, acme_driver_preload + MD_PROTO_ACME, acme_driver_init, acme_driver_renew, + acme_driver_preload_init, acme_driver_preload }; apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) diff --git a/modules/md/md_acme_order.c b/modules/md/md_acme_order.c index 9314f20678..2907d5296c 100644 --- a/modules/md/md_acme_order.c +++ b/modules/md/md_acme_order.c @@ -447,7 +447,7 @@ apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *a if (APR_SUCCESS != (rv = md_acme_authz_retrieve(acme, p, url, &authz))) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: check authz for %s", md->name, authz->domain); - goto out; + goto leave; } switch (authz->state) { @@ -459,21 +459,28 @@ apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *a md->pkey_spec, md->acme_tls_1_domains, env, p, &setup_token, result); if (APR_SUCCESS != rv) { - goto out; + goto leave; } add_setup_token(order, setup_token); md_acme_order_save(store, p, MD_SG_STAGING, md->name, order, 0); break; + case MD_ACME_AUTHZ_S_INVALID: + rv = APR_EINVAL; + if (authz->error_type) { + md_result_problem_set(result, rv, authz->error_type, authz->error_detail, NULL); + goto leave; + } + /* fall through */ default: rv = APR_EINVAL; md_result_printf(result, rv, "unexpected AUTHZ state %d for domain %s", authz->state, authz->domain); md_result_log(result, MD_LOG_ERR); - goto out; + goto leave; } } -out: +leave: return rv; } @@ -502,6 +509,16 @@ static apr_status_t check_challenges(void *baton, int attempt) md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ctx->p, "%s: status pending at %s", authz->domain, authz->url); goto leave; + case MD_ACME_AUTHZ_S_INVALID: + rv = APR_EINVAL; + if (!authz->error_type) { + md_result_printf(ctx->result, rv, + "domain authorization for %s failed, CA consideres " + "answer to challenge invalid, no error given", + authz->domain); + } + md_result_log(ctx->result, MD_LOG_ERR); + goto leave; default: rv = APR_EINVAL; md_result_printf(ctx->result, rv, @@ -531,7 +548,7 @@ apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acm md_result_activity_printf(result, "Monitoring challenge status for %s", md->name); rv = md_util_try(check_challenges, &ctx, 0, timeout, 0, 0, 1); - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, "%s: checked authorizations", md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: checked authorizations", md->name); return rv; } diff --git a/modules/md/md_acmev1_drive.c b/modules/md/md_acmev1_drive.c index d52e195755..30e2add19d 100644 --- a/modules/md/md_acmev1_drive.c +++ b/modules/md/md_acmev1_drive.c @@ -139,7 +139,7 @@ apr_status_t md_acmev1_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m apr_status_t rv = APR_SUCCESS; const char *required; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: (ACMEv1) need certificate", d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: (ACMEv1) need certificate", d->md->name); /* Chose (or create) and ACME account to use */ if (APR_SUCCESS != (rv = md_acme_drive_set_acct(d, result))) goto leave; @@ -147,7 +147,7 @@ apr_status_t md_acmev1_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m /* Check that the account agreed to the terms-of-service, otherwise * requests for new authorizations are denied. ToS may change during the * lifetime of an account */ - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: (ACMEv1) check Tems-of-Service agreement", d->md->name); rv = md_acme_check_agreement(ad->acme, d->p, ad->md->ca_agreement, &required); diff --git a/modules/md/md_acmev2_drive.c b/modules/md/md_acmev2_drive.c index cd5214d5d4..342e60a90f 100644 --- a/modules/md/md_acmev2_drive.c +++ b/modules/md/md_acmev2_drive.c @@ -95,7 +95,7 @@ apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m { apr_status_t rv = APR_SUCCESS; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: (ACMEv2) need certificate", d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: (ACMEv2) need certificate", d->md->name); /* Chose (or create) and ACME account to use */ rv = md_acme_drive_set_acct(d, result); @@ -149,7 +149,7 @@ apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m rv = md_acme_drive_setup_certificate(d, result); if (APR_SUCCESS != rv) goto leave; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: finalized order", d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: finalized order", d->md->name); rv = md_acme_order_await_valid(ad->order, ad->acme, d->md, ad->authz_monitor_timeout, result, d->p); diff --git a/modules/md/md_core.c b/modules/md/md_core.c index 080e542696..5d59b3d593 100644 --- a/modules/md/md_core.c +++ b/modules/md/md_core.c @@ -107,6 +107,7 @@ md_t *md_create_empty(apr_pool_t *p) md->must_staple = -1; md->transitive = -1; md->acme_tls_1_domains = apr_array_make(p, 5, sizeof(const char *)); + md->stapling = -1; md->defn_name = "unknown"; md->defn_line_number = 0; } @@ -143,38 +144,6 @@ int md_contains_domains(const md_t *md1, const md_t *md2) return 0; } -md_t *md_find_closest_match(apr_array_header_t *mds, const md_t *md) -{ - md_t *candidate, *m; - apr_size_t cand_n, n; - int i; - - candidate = md_get_by_name(mds, md->name); - if (!candidate) { - /* try to find an instance that contains all domain names from md */ - for (i = 0; i < mds->nelts; ++i) { - m = APR_ARRAY_IDX(mds, i, md_t *); - if (md_contains_domains(m, md)) { - return m; - } - } - /* no matching name and no md in the list has all domains. - * We consider that managed domain as closest match that contains at least one - * domain name from md, ONLY if there is no other one that also has. - */ - cand_n = 0; - for (i = 0; i < mds->nelts; ++i) { - m = APR_ARRAY_IDX(mds, i, md_t *); - n = md_common_name_count(md, m); - if (n > cand_n) { - candidate = m; - cand_n = n; - } - } - } - return candidate; -} - md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name) { int i; @@ -268,6 +237,7 @@ md_t *md_clone(apr_pool_t *p, const md_t *src) md->ca_challenges = md_array_str_clone(p, src->ca_challenges); } md->acme_tls_1_domains = md_array_str_compact(p, src->acme_tls_1_domains, 0); + md->stapling = src->stapling; if (src->cert_file) md->cert_file = apr_pstrdup(p, src->cert_file); if (src->pkey_file) md->pkey_file = apr_pstrdup(p, src->pkey_file); } @@ -315,10 +285,10 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p) break; } md_json_setb(md->must_staple > 0, json, MD_KEY_MUST_STAPLE, NULL); - if (!apr_is_empty_array(md->acme_tls_1_domains)) - md_json_setsa(md->acme_tls_1_domains, json, MD_KEY_PROTO, MD_KEY_ACME_TLS_1, NULL); + md_json_setsa(md->acme_tls_1_domains, json, MD_KEY_PROTO, MD_KEY_ACME_TLS_1, NULL); md_json_sets(md->cert_file, json, MD_KEY_CERT_FILE, NULL); md_json_sets(md->pkey_file, json, MD_KEY_PKEY_FILE, NULL); + md_json_setb(md->stapling > 0, json, MD_KEY_STAPLING, NULL); return json; } return NULL; @@ -365,6 +335,7 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) md->cert_file = md_json_dups(p, json, MD_KEY_CERT_FILE, NULL); md->pkey_file = md_json_dups(p, json, MD_KEY_PKEY_FILE, NULL); + md->stapling = (int)md_json_getb(json, MD_KEY_STAPLING, NULL); return md; } diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c index 57c32a2bfb..55155975de 100644 --- a/modules/md/md_crypt.c +++ b/modules/md/md_crypt.c @@ -155,7 +155,7 @@ apr_status_t md_crypt_init(apr_pool_t *pool) static apr_status_t fwrite_buffer(void *baton, apr_file_t *f, apr_pool_t *p) { - md_data *buf = baton; + md_data_t *buf = baton; apr_size_t wlen; (void)p; @@ -253,6 +253,11 @@ static apr_time_t md_asn1_time_get(const ASN1_TIME* time) #endif } +apr_time_t md_asn1_generalized_time_get(void *ASN1_GENERALIZEDTIME) +{ + return md_asn1_time_get(ASN1_GENERALIZEDTIME); +} + /**************************************************************************************************/ /* private keys */ @@ -384,7 +389,7 @@ apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p, return rv; } -static apr_status_t pkey_to_buffer(md_data *buf, md_pkey_t *pkey, apr_pool_t *p, +static apr_status_t pkey_to_buffer(md_data_t *buf, md_pkey_t *pkey, apr_pool_t *p, const char *pass, apr_size_t pass_len) { BIO *bio = BIO_new(BIO_s_mem()); @@ -435,7 +440,7 @@ apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, const char *pass_phrase, apr_size_t pass_len, const char *fname, apr_fileperms_t perms) { - md_data buffer; + md_data_t buffer; apr_status_t rv; if (APR_SUCCESS == (rv = pkey_to_buffer(&buffer, pkey, p, pass_phrase, pass_len))) { @@ -507,12 +512,14 @@ static void RSA_get0_key(const RSA *r, static const char *bn64(const BIGNUM *b, apr_pool_t *p) { if (b) { - apr_size_t len = (apr_size_t)BN_num_bytes(b); - char *buffer = apr_pcalloc(p, len); - if (buffer) { - BN_bn2bin(b, (unsigned char *)buffer); - return md_util_base64url_encode(buffer, len, p); - } + md_data_t buffer; + + buffer.len = (apr_size_t)BN_num_bytes(b); + buffer.data = apr_pcalloc(p, buffer.len); + if (buffer.data) { + BN_bn2bin(b, (unsigned char *)buffer.data); + return md_util_base64url_encode(&buffer, p); + } } return NULL; } @@ -545,21 +552,23 @@ apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t * const char *d, size_t dlen) { EVP_MD_CTX *ctx = NULL; - char *buffer; + md_data_t buffer; unsigned int blen; const char *sign64 = NULL; apr_status_t rv = APR_ENOMEM; - buffer = apr_pcalloc(p, (apr_size_t)EVP_PKEY_size(pkey->pkey)); - if (buffer) { + buffer.len = (apr_size_t)EVP_PKEY_size(pkey->pkey); + buffer.data = apr_pcalloc(p, buffer.len); + if (buffer.data) { ctx = EVP_MD_CTX_create(); if (ctx) { rv = APR_ENOTIMPL; if (EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) { rv = APR_EGENERAL; if (EVP_SignUpdate(ctx, d, dlen)) { - if (EVP_SignFinal(ctx, (unsigned char*)buffer, &blen, pkey->pkey)) { - sign64 = md_util_base64url_encode(buffer, blen, p); + if (EVP_SignFinal(ctx, (unsigned char*)buffer.data, &blen, pkey->pkey)) { + buffer.len = blen; + sign64 = md_util_base64url_encode(&buffer, p); if (sign64) { rv = APR_SUCCESS; } @@ -581,10 +590,10 @@ apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t * return rv; } -static apr_status_t sha256_digest(md_data **pdigest, apr_pool_t *p, const md_data *buf) +static apr_status_t sha256_digest(md_data_t **pdigest, apr_pool_t *p, const md_data_t *buf) { EVP_MD_CTX *ctx = NULL; - md_data *digest; + md_data_t *digest; apr_status_t rv = APR_ENOMEM; unsigned int dlen; @@ -614,14 +623,14 @@ leave: return rv; } -apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, const md_data *d) +apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, const md_data_t *d) { const char *digest64 = NULL; - md_data *digest; + md_data_t *digest; apr_status_t rv; if (APR_SUCCESS == (rv = sha256_digest(&digest, p, d))) { - if (NULL == (digest64 = md_util_base64url_encode(digest->data, digest->len, p))) { + if (NULL == (digest64 = md_util_base64url_encode(digest, p))) { rv = APR_EGENERAL; } } @@ -630,9 +639,9 @@ apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, con } apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, - const md_data *data) + const md_data_t *data) { - md_data *digest; + md_data_t *digest; apr_status_t rv; if (APR_SUCCESS == (rv = sha256_digest(&digest, p, data))) { @@ -661,19 +670,19 @@ static apr_status_t cert_cleanup(void *data) return APR_SUCCESS; } -static md_cert_t *make_cert(apr_pool_t *p, X509 *x509) +md_cert_t *md_cert_wrap(apr_pool_t *p, void *x509) { md_cert_t *cert = apr_pcalloc(p, sizeof(*cert)); cert->pool = p; cert->x509 = x509; - apr_pool_cleanup_register(p, cert, cert_cleanup, apr_pool_cleanup_null); - return cert; } -void md_cert_free(md_cert_t *cert) +md_cert_t *md_cert_make(apr_pool_t *p, void *x509) { - cert_cleanup(cert); + md_cert_t *cert = md_cert_wrap(p, x509); + apr_pool_cleanup_register(p, cert, cert_cleanup, apr_pool_cleanup_null); + return cert; } void *md_cert_get_X509(const md_cert_t *cert) @@ -684,16 +693,15 @@ void *md_cert_get_X509(const md_cert_t *cert) const char *md_cert_get_serial_number(const md_cert_t *cert, apr_pool_t *p) { const char *s = ""; + BIGNUM *bn; + const char *serial; const ASN1_INTEGER *ai = X509_get_serialNumber(cert->x509); if (ai) { - BIGNUM *bn; - const char *hex; - bn = ASN1_INTEGER_to_BN(ai, NULL); - hex = BN_bn2hex(bn); - s = apr_pstrdup(p, hex); + serial = BN_bn2hex(bn); + s = apr_pstrdup(p, serial); + OPENSSL_free((void*)serial); OPENSSL_free((void*)bn); - OPENSSL_free((void*)hex); } return s; } @@ -831,7 +839,7 @@ apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname) x509 = PEM_read_X509(f, NULL, NULL, NULL); rv = fclose(f); if (x509 != NULL) { - cert = make_cert(p, x509); + cert = md_cert_make(p, x509); } else { rv = APR_EINVAL; @@ -842,7 +850,7 @@ apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname) return rv; } -static apr_status_t cert_to_buffer(md_data *buffer, const md_cert_t *cert, apr_pool_t *p) +static apr_status_t cert_to_buffer(md_data_t *buffer, const md_cert_t *cert, apr_pool_t *p) { BIO *bio = BIO_new(BIO_s_mem()); int i; @@ -871,7 +879,7 @@ static apr_status_t cert_to_buffer(md_data *buffer, const md_cert_t *cert, apr_p apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, const char *fname, apr_fileperms_t perms) { - md_data buffer; + md_data_t buffer; apr_status_t rv; if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) { @@ -882,20 +890,20 @@ apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, apr_status_t md_cert_to_base64url(const char **ps64, const md_cert_t *cert, apr_pool_t *p) { - md_data buffer; + md_data_t buffer; apr_status_t rv; if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) { - *ps64 = md_util_base64url_encode(buffer.data, buffer.len, p); + *ps64 = md_util_base64url_encode(&buffer, p); return APR_SUCCESS; } *ps64 = NULL; return rv; } -apr_status_t md_cert_to_sha256_digest(md_data **pdigest, const md_cert_t *cert, apr_pool_t *p) +apr_status_t md_cert_to_sha256_digest(md_data_t **pdigest, const md_cert_t *cert, apr_pool_t *p) { - md_data *digest; + md_data_t *digest; unsigned int dlen; apr_status_t rv = APR_ENOMEM; @@ -914,7 +922,7 @@ leave: apr_status_t md_cert_to_sha256_fingerprint(const char **pfinger, const md_cert_t *cert, apr_pool_t *p) { - md_data *digest; + md_data_t *digest; apr_status_t rv; rv = md_cert_to_sha256_digest(&digest, cert, p); @@ -937,7 +945,7 @@ static int md_cert_read_pem(BIO *bf, apr_pool_t *p, md_cert_t **pcert) rv = APR_ENOENT; goto out; } - cert = make_cert(p, x509); + cert = md_cert_make(p, x509); rv = APR_SUCCESS; out: @@ -974,7 +982,7 @@ apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *p, goto out; } else { - cert = make_cert(p, x509); + cert = md_cert_make(p, x509); rv = APR_SUCCESS; md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "cert parsed"); } @@ -1065,7 +1073,7 @@ apr_status_t md_chain_fappend(struct apr_array_header_t *certs, apr_pool_t *p, c if (rv == APR_SUCCESS) { ERR_clear_error(); while (NULL != (x509 = PEM_read_X509(f, NULL, NULL, NULL))) { - cert = make_cert(p, x509); + cert = md_cert_make(p, x509); APR_ARRAY_PUSH(certs, md_cert_t *) = cert; } fclose(f); @@ -1248,12 +1256,13 @@ apr_status_t md_cert_req_create(const char **pcsr_der_64, const char *name, apr_array_header_t *domains, int must_staple, md_pkey_t *pkey, apr_pool_t *p) { - const char *s, *csr_der, *csr_der_64 = NULL; + const char *s, *csr_der_64 = NULL; const unsigned char *domain; X509_REQ *csr; X509_NAME *n = NULL; STACK_OF(X509_EXTENSION) *exts = NULL; apr_status_t rv; + md_data_t csr_der; int csr_der_len; assert(domains->nelts > 0); @@ -1305,12 +1314,13 @@ apr_status_t md_cert_req_create(const char **pcsr_der_64, const char *name, md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: der length", name); rv = APR_EGENERAL; goto out; } - s = csr_der = apr_pcalloc(p, (apr_size_t)csr_der_len + 1); + csr_der.len = (apr_size_t)csr_der_len; + s = csr_der.data = apr_pcalloc(p, csr_der.len + 1); if (i2d_X509_REQ(csr, (unsigned char**)&s) != csr_der_len) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: csr der enc", name); rv = APR_EGENERAL; goto out; } - csr_der_64 = md_util_base64url_encode(csr_der, (apr_size_t)csr_der_len, p); + csr_der_64 = md_util_base64url_encode(&csr_der, p); rv = APR_SUCCESS; out: @@ -1418,7 +1428,7 @@ apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, rv = APR_EGENERAL; goto out; } - cert = make_cert(p, x); + cert = md_cert_make(p, x); rv = APR_SUCCESS; out: @@ -1470,7 +1480,7 @@ apr_status_t md_cert_make_tls_alpn_01(md_cert_t **pcert, const char *domain, rv = APR_EGENERAL; goto out; } - cert = make_cert(p, x); + cert = md_cert_make(p, x); rv = APR_SUCCESS; out: diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h index cc024533e8..7d60af3ae2 100644 --- a/modules/md/md_crypt.h +++ b/modules/md/md_crypt.h @@ -24,7 +24,7 @@ struct md_t; struct md_http_response_t; struct md_cert_t; struct md_pkey_t; -struct md_data; +struct md_data_t; /**************************************************************************************************/ @@ -32,12 +32,14 @@ struct md_data; apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p); +apr_time_t md_asn1_generalized_time_get(void *ASN1_GENERALIZEDTIME); + /**************************************************************************************************/ /* digests */ apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, - const struct md_data *data); + const struct md_data_t *data); apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, - const struct md_data *data); + const struct md_data_t *data); #define MD_DATA_SET_STR(d, s) do { (d)->data = (s); (d)->len = strlen(s); } while(0) @@ -97,7 +99,18 @@ typedef enum { MD_CERT_EXPIRED } md_cert_state_t; -void md_cert_free(md_cert_t *cert); +/** + * Create a holder of the certificate that will free its memmory when the + * pool is destroyed. + */ +md_cert_t *md_cert_make(apr_pool_t *p, void *x509); + +/** + * Wrap a x509 certificate into our own structure, without taking ownership + * of its memory. The caller remains responsible. + */ +md_cert_t *md_cert_wrap(apr_pool_t *p, void *x509); + void *md_cert_get_X509(const md_cert_t *cert); apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname); @@ -136,7 +149,7 @@ apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, const md_cert_t apr_status_t md_cert_to_base64url(const char **ps64, const md_cert_t *cert, apr_pool_t *p); apr_status_t md_cert_from_base64url(md_cert_t **pcert, const char *s64, apr_pool_t *p); -apr_status_t md_cert_to_sha256_digest(struct md_data **pdigest, const md_cert_t *cert, apr_pool_t *p); +apr_status_t md_cert_to_sha256_digest(struct md_data_t **pdigest, const md_cert_t *cert, apr_pool_t *p); apr_status_t md_cert_to_sha256_fingerprint(const char **pfinger, const md_cert_t *cert, apr_pool_t *p); const char *md_cert_get_serial_number(const md_cert_t *cert, apr_pool_t *p); @@ -181,9 +194,9 @@ typedef struct md_sct md_sct; struct md_sct { int version; apr_time_t timestamp; - struct md_data *logid; + struct md_data_t *logid; int signature_type_nid; - struct md_data *signature; + struct md_data_t *signature; }; #endif /* md_crypt_h */ diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c index a7aad7d279..9c4be13b69 100644 --- a/modules/md/md_curl.c +++ b/modules/md/md_curl.c @@ -24,13 +24,14 @@ #include "md_http.h" #include "md_log.h" +#include "md_util.h" #include "md_curl.h" /**************************************************************************************************/ /* md_http curl implementation */ -static apr_status_t curl_status(int curl_code) +static apr_status_t curl_status(unsigned int curl_code) { switch (curl_code) { case CURLE_OK: return APR_SUCCESS; @@ -49,6 +50,15 @@ static apr_status_t curl_status(int curl_code) } } +typedef struct { + CURL *curl; + CURLM *curlm; + struct curl_slist *req_hdrs; + md_http_response_t *response; + apr_status_t rv; + int status_fired; +} md_curl_internals_t; + static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton) { apr_bucket_brigade *body = baton; @@ -92,7 +102,8 @@ static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton) static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) { - md_http_response_t *res = baton; + md_curl_internals_t *internals = baton; + md_http_response_t *res = internals->response; size_t blen = len * nmemb; apr_status_t rv; @@ -100,7 +111,7 @@ static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) if (res->req->resp_limit) { apr_off_t body_len = 0; apr_brigade_length(res->body, 0, &body_len); - if (body_len + (apr_off_t)len > res->req->resp_limit) { + if (body_len + (apr_off_t)blen > res->req->resp_limit) { return 0; /* signal curl failure */ } } @@ -115,7 +126,8 @@ static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton) { - md_http_response_t *res = baton; + md_curl_internals_t *internals = baton; + md_http_response_t *res = internals->response; size_t len, clen = elen * nmemb; const char *name = NULL, *value = "", *b = buffer; apr_size_t i; @@ -142,24 +154,6 @@ static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton) return clen; } -static apr_status_t curl_init(md_http_request_t *req) -{ - CURL *curl = curl_easy_init(); - if (!curl) { - return APR_EGENERAL; - } - - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL); - curl_easy_setopt(curl, CURLOPT_READFUNCTION, req_data_cb); - curl_easy_setopt(curl, CURLOPT_READDATA, NULL); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, resp_data_cb); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL); - - req->internals = curl; - return APR_SUCCESS; -} - typedef struct { md_http_request_t *req; struct curl_slist *hdrs; @@ -181,24 +175,96 @@ static int curlify_headers(void *baton, const char *key, const char *value) return 1; } -static apr_status_t curl_perform(md_http_request_t *req) +/* Convert timeout values for curl. Since curl uses 0 to disable + * timeout, return at least 1 if the apr_time_t value is non-zero. */ +static long timeout_msec(apr_time_t timeout) { - apr_status_t rv = APR_SUCCESS; - CURLcode curle; - md_http_response_t *res; - CURL *curl; - struct curl_slist *req_hdrs = NULL; + long ms = (long)apr_time_as_msec(timeout); + return ms? ms : (timeout? 1 : 0); +} - if (APR_SUCCESS != (rv = curl_init(req))) return rv; - curl = req->internals; +static long timeout_sec(apr_time_t timeout) +{ + long s = (long)apr_time_sec(timeout); + return s? s : (timeout? 1 : 0); +} + +static int curl_debug_log(CURL *curl, curl_infotype type, char *data, size_t size, void *baton) +{ + md_http_request_t *req = baton; - res = apr_pcalloc(req->pool, sizeof(*res)); + (void)curl; + switch (type) { + case CURLINFO_TEXT: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: info %s", req->id, apr_pstrndup(req->pool, data, size)); + break; + case CURLINFO_HEADER_OUT: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: header --> %s", req->id, apr_pstrndup(req->pool, data, size)); + break; + case CURLINFO_HEADER_IN: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: header <-- %s", req->id, apr_pstrndup(req->pool, data, size)); + break; + case CURLINFO_DATA_OUT: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: data --> %ld bytes", req->id, (long)size); + if (md_log_is_level(req->pool, MD_LOG_TRACE5)) { + md_data_t d; + const char *s; + d.data = data; + d.len = size; + md_data_to_hex(&s, 0, req->pool, &d); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE5, 0, req->pool, + "req[%d]: data(hex) --> %s", req->id, s); + } + break; + case CURLINFO_DATA_IN: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: data <-- %ld bytes", req->id, (long)size); + if (md_log_is_level(req->pool, MD_LOG_TRACE5)) { + md_data_t d; + const char *s; + d.data = data; + d.len = size; + md_data_to_hex(&s, 0, req->pool, &d); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE5, 0, req->pool, + "req[%d]: data(hex) <-- %s", req->id, s); + } + break; + default: + break; + } + return 0; +} + +static apr_status_t internals_setup(md_http_request_t *req) +{ + md_curl_internals_t *internals; + CURL *curl; + apr_status_t rv = APR_SUCCESS; - res->req = req; - res->rv = APR_SUCCESS; - res->status = 400; - res->headers = apr_table_make(req->pool, 5); - res->body = apr_brigade_create(req->pool, req->bucket_alloc); + curl = curl_easy_init(); + if (!curl) { + rv = APR_EGENERAL; + goto leave; + } + internals = apr_pcalloc(req->pool, sizeof(*internals)); + internals->curl = curl; + + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL); + curl_easy_setopt(curl, CURLOPT_READFUNCTION, req_data_cb); + curl_easy_setopt(curl, CURLOPT_READDATA, NULL); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, resp_data_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL); + + internals->response = apr_pcalloc(req->pool, sizeof(md_http_response_t)); + internals->response->req = req; + internals->response->status = 400; + internals->response->headers = apr_table_make(req->pool, 5); + internals->response->body = apr_brigade_create(req->pool, req->bucket_alloc); curl_easy_setopt(curl, CURLOPT_URL, req->url); if (!apr_strnatcasecmp("GET", req->method)) { @@ -213,9 +279,20 @@ static apr_status_t curl_perform(md_http_request_t *req) else { curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); } - curl_easy_setopt(curl, CURLOPT_HEADERDATA, res); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, internals); curl_easy_setopt(curl, CURLOPT_READDATA, req->body); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, res); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, internals); + + if (req->timeout.overall > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_msec(req->timeout.overall)); + } + if (req->timeout.connect > 0) { + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_msec(req->timeout.connect)); + } + if (req->timeout.stalled > 0) { + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, req->timeout.stall_bytes_per_sec); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, timeout_sec(req->timeout.stalled)); + } if (req->user_agent) { curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent); @@ -230,47 +307,244 @@ static apr_status_t curl_perform(md_http_request_t *req) ctx.hdrs = NULL; ctx.rv = APR_SUCCESS; apr_table_do(curlify_headers, &ctx, req->headers, NULL); - req_hdrs = ctx.hdrs; + internals->req_hdrs = ctx.hdrs; if (ctx.rv == APR_SUCCESS) { - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_hdrs); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, internals->req_hdrs); } } - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, - "request --> %s %s", req->method, req->url); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, + "req[%d]: %s %s", req->id, req->method, req->url); - if (md_log_is_level(req->pool, MD_LOG_TRACE3)) { + if (md_log_is_level(req->pool, MD_LOG_TRACE4)) { curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_log); + curl_easy_setopt(curl, CURLOPT_DEBUGDATA, req); } - curle = curl_easy_perform(curl); - res->rv = curl_status(curle); +leave: + req->internals = (APR_SUCCESS == rv)? internals : NULL; + return rv; +} + +static apr_status_t update_status(md_http_request_t *req) +{ + md_curl_internals_t *internals = req->internals; + long l; + apr_status_t rv = APR_SUCCESS; + + if (internals) { + rv = curl_status(curl_easy_getinfo(internals->curl, CURLINFO_RESPONSE_CODE, &l)); + if (APR_SUCCESS == rv) { + internals->response->status = (int)l; + } + } + return rv; +} + +static void fire_status(md_http_request_t *req, apr_status_t rv) +{ + md_curl_internals_t *internals = req->internals; + + if (internals && !internals->status_fired) { + internals->status_fired = 1; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, req->pool, + "req[%d] fire callbacks", req->id); + if ((APR_SUCCESS == rv) && req->cb.on_response) { + rv = req->cb.on_response(internals->response, req->cb.on_response_data); + } - if (APR_SUCCESS == res->rv) { - long l; - res->rv = curl_status(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &l)); - if (APR_SUCCESS == res->rv) { - res->status = (int)l; + internals->rv = rv; + if (req->cb.on_status) { + req->cb.on_status(req, rv, req->cb.on_status_data); } - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, res->rv, req->pool, - "request <-- %d", res->status); } - else { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, res->rv, req->pool, - "request failed(%d): %s", curle, - curl_easy_strerror(curle)); +} + +static apr_status_t md_curl_perform(md_http_request_t *req) +{ + apr_status_t rv = APR_SUCCESS; + CURLcode curle; + md_curl_internals_t *internals; + long l; + + if (APR_SUCCESS != (rv = internals_setup(req))) goto leave; + internals = req->internals; + + curle = curl_easy_perform(internals->curl); + + rv = curl_status(curle); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->pool, + "request failed(%d): %s", curle, curl_easy_strerror(curle)); + goto leave; } - if (req->cb) { - res->rv = req->cb(res); + rv = curl_status(curl_easy_getinfo(internals->curl, CURLINFO_RESPONSE_CODE, &l)); + if (APR_SUCCESS == rv) { + internals->response->status = (int)l; } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->pool, "request <-- %d", + internals->response->status); - rv = res->rv; + if (req->cb.on_response) { + rv = req->cb.on_response(internals->response, req->cb.on_response_data); + req->cb.on_response = NULL; + } + +leave: + fire_status(req, rv); md_http_req_destroy(req); - if (req_hdrs) { - curl_slist_free_all(req_hdrs); + return rv; +} + +static md_http_request_t *find_curl_request(apr_array_header_t *requests, CURL *curl) +{ + md_http_request_t *req; + md_curl_internals_t *internals; + int i; + + for (i = 0; i < requests->nelts; ++i) { + req = APR_ARRAY_IDX(requests, i, md_http_request_t*); + internals = req->internals; + if (internals && internals->curl == curl) { + return req; + } + } + return NULL; +} + +static void add_to_curlm(md_http_request_t *req, CURLM *curlm) +{ + md_curl_internals_t *internals = req->internals; + + if (curlm && internals && internals->curlm == NULL) { + curl_multi_add_handle(curlm, internals->curl); + internals->curlm = curlm; + } +} + +static void remove_from_curlm(md_http_request_t *req, CURLM *curlm) +{ + md_curl_internals_t *internals = req->internals; + + if (curlm && internals && internals->curlm == curlm) { + curl_multi_remove_handle(curlm, internals->curl); + internals->curlm = NULL; + } +} + +static apr_status_t md_curl_multi_perform(md_http_t *http, apr_pool_t *p, + md_http_next_req *nextreq, void *baton) +{ + md_http_request_t *req; + CURLM *curlm = NULL; + CURLMcode mc; + struct CURLMsg *curlmsg; + apr_array_header_t *requests; + int i, running, numfds, slowdown, msgcount; + apr_status_t rv; + + requests = apr_array_make(p, 10, sizeof(md_http_request_t*)); + curlm = curl_multi_init(); + if (!curlm) { + rv = APR_ENOMEM; + goto leave; } + running = 1; + slowdown = 0; + while(1) { + while (1) { + /* fetch as many requests as nextreq gives us */ + rv = nextreq(&req, baton, http, requests->nelts); + + if (APR_SUCCESS == rv) { + if (APR_SUCCESS != (rv = internals_setup(req))) { + if (req->cb.on_status) req->cb.on_status(req, rv, req->cb.on_status_data); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: setup failed", requests->nelts); + } + else { + APR_ARRAY_PUSH(requests, md_http_request_t*) = req; + add_to_curlm(req, curlm); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: added request", requests->nelts); + } + continue; + } + else if (APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, + "multi_perform[%d reqs]: no more requests", requests->nelts); + if (!running) { + goto leave; + } + break; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: nextreq() failed", requests->nelts); + goto leave; + } + } + + mc = curl_multi_perform(curlm, &running); + if (CURLM_OK == mc) { + mc = curl_multi_wait(curlm, NULL, 0, 1000, &numfds); + if (numfds) slowdown = 0; + } + if (CURLM_OK != mc) { + rv = APR_ECONNABORTED; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "multi_perform[%d reqs] failed(%d): %s", + requests->nelts, mc, curl_multi_strerror(mc)); + goto leave; + } + if (!numfds) { + /* no activity on any connection, timeout */ + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, + "multi_perform[%d reqs]: slowdown %d", requests->nelts, slowdown); + if (slowdown) apr_sleep(apr_time_from_msec(100)); + ++slowdown; + } + + /* process status messages, e.g. that a request is done */ + while (1) { + curlmsg = curl_multi_info_read(curlm, &msgcount); + if (!curlmsg) break; + if (curlmsg->msg == CURLMSG_DONE) { + req = find_curl_request(requests, curlmsg->easy_handle); + if (req) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, + "multi_perform[%d reqs]: req[%d] done", + requests->nelts, req->id); + update_status(req); + fire_status(req, curl_status(curlmsg->data.result)); + remove_from_curlm(req, curlm); + md_array_remove(requests, req); + md_http_req_destroy(req); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "multi_perform[%d reqs]: req done, but not found by handle", + requests->nelts); + } + } + } + assert(running == requests->nelts); + }; + +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: leaving", requests->nelts); + for (i = 0; i < requests->nelts; ++i) { + req = APR_ARRAY_IDX(requests, i, md_http_request_t*); + fire_status(req, APR_SUCCESS); + remove_from_curlm(req, curlm); + md_http_req_destroy(req); + } + if (curlm) curl_multi_cleanup(curlm); return rv; } @@ -284,18 +558,21 @@ static apr_status_t md_curl_init(void) { return APR_SUCCESS; } -static void curl_req_cleanup(md_http_request_t *req) +static void md_curl_req_cleanup(md_http_request_t *req) { - if (req->internals) { - curl_easy_cleanup(req->internals); + md_curl_internals_t *internals = req->internals; + if (internals) { + if (internals->curl) curl_easy_cleanup(internals->curl); + if (internals->req_hdrs) curl_slist_free_all(internals->req_hdrs); req->internals = NULL; } } static md_http_impl_t impl = { md_curl_init, - curl_req_cleanup, - curl_perform + md_curl_req_cleanup, + md_curl_perform, + md_curl_multi_perform, }; md_http_impl_t * md_curl_get_impl(apr_pool_t *p) diff --git a/modules/md/md_http.c b/modules/md/md_http.c index c9383f88c6..027de593d5 100644 --- a/modules/md/md_http.c +++ b/modules/md/md_http.c @@ -22,14 +22,17 @@ #include "md_http.h" #include "md_log.h" +#include "md_util.h" struct md_http_t { apr_pool_t *pool; apr_bucket_alloc_t *bucket_alloc; + int next_id; apr_off_t resp_limit; md_http_impl_t *impl; const char *user_agent; const char *proxy_url; + md_http_timeouts_t timeout; }; static md_http_impl_t *cur_impl; @@ -81,9 +84,82 @@ void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit) http->resp_limit = resp_limit; } +void md_http_set_timeout_default(md_http_t *http, apr_time_t timeout) +{ + http->timeout.overall = timeout; +} + +void md_http_set_timeout(md_http_request_t *req, apr_time_t timeout) +{ + req->timeout.overall = timeout; +} + +void md_http_set_connect_timeout_default(md_http_t *http, apr_time_t timeout) +{ + http->timeout.connect = timeout; +} + +void md_http_set_connect_timeout(md_http_request_t *req, apr_time_t timeout) +{ + req->timeout.connect = timeout; +} + +void md_http_set_stalling_default(md_http_t *http, long bytes_per_sec, apr_time_t timeout) +{ + http->timeout.stall_bytes_per_sec = bytes_per_sec; + http->timeout.stalled = timeout; +} + +void md_http_set_stalling(md_http_request_t *req, long bytes_per_sec, apr_time_t timeout) +{ + req->timeout.stall_bytes_per_sec = bytes_per_sec; + req->timeout.stalled = timeout; +} + +static apr_status_t req_set_body(md_http_request_t *req, const char *content_type, + apr_bucket_brigade *body, apr_off_t body_len, + int detect_len) +{ + apr_status_t rv = APR_SUCCESS; + + if (body && detect_len) { + rv = apr_brigade_length(body, 1, &body_len); + if (rv != APR_SUCCESS) { + return rv; + } + } + + req->body = body; + req->body_len = body? body_len : 0; + if (content_type) { + apr_table_set(req->headers, "Content-Type", content_type); + } + else { + apr_table_unset(req->headers, "Content-Type"); + } + return rv; +} + +static apr_status_t req_set_body_data(md_http_request_t *req, const char *content_type, + const md_data_t *body) +{ + apr_bucket_brigade *bbody = NULL; + apr_status_t rv; + + if (body && body->len > 0) { + bbody = apr_brigade_create(req->pool, req->http->bucket_alloc); + rv = apr_brigade_write(bbody, NULL, NULL, body->data, body->len); + if (rv != APR_SUCCESS) { + md_http_req_destroy(req); + return rv; + } + } + return req_set_body(req, content_type, bbody, body? (apr_off_t)body->len : 0, 0); +} + static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, - const char *method, const char *url, struct apr_table_t *headers, - md_http_cb *cb, void *baton) + const char *method, const char *url, + struct apr_table_t *headers) { md_http_request_t *req; apr_pool_t *pool; @@ -96,17 +172,16 @@ static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, req = apr_pcalloc(pool, sizeof(*req)); req->pool = pool; + req->id = http->next_id++; req->bucket_alloc = http->bucket_alloc; req->http = http; req->method = method; req->url = url; req->headers = headers? apr_table_copy(req->pool, headers) : apr_table_make(req->pool, 5); req->resp_limit = http->resp_limit; - req->cb = cb; - req->baton = baton; req->user_agent = http->user_agent; req->proxy_url = http->proxy_url; - + req->timeout = http->timeout; *preq = req; return rv; } @@ -120,107 +195,157 @@ void md_http_req_destroy(md_http_request_t *req) apr_pool_destroy(req->pool); } -static apr_status_t schedule(md_http_request_t *req, - apr_bucket_brigade *body, int detect_clen) +void md_http_set_on_status_cb(md_http_request_t *req, md_http_status_cb *cb, void *baton) { - apr_status_t rv; - - req->body = body; - req->body_len = body? -1 : 0; + req->cb.on_status = cb; + req->cb.on_status_data = baton; +} - if (req->body && detect_clen) { - rv = apr_brigade_length(req->body, 1, &req->body_len); - if (rv != APR_SUCCESS) { - md_http_req_destroy(req); - return rv; - } - } - +void md_http_set_on_response_cb(md_http_request_t *req, md_http_response_cb *cb, void *baton) +{ + req->cb.on_response = cb; + req->cb.on_response_data = baton; +} + +static void req_init_cl(md_http_request_t *req) +{ if (req->body_len == 0 && apr_strnatcasecmp("GET", req->method)) { apr_table_setn(req->headers, "Content-Length", "0"); } else if (req->body_len > 0) { apr_table_setn(req->headers, "Content-Length", apr_off_t_toa(req->pool, req->body_len)); } - +} + +apr_status_t md_http_perform(md_http_request_t *req) +{ + req_init_cl(req); return req->http->impl->perform(req); } -apr_status_t md_http_GET(struct md_http_t *http, - const char *url, struct apr_table_t *headers, - md_http_cb *cb, void *baton) +typedef struct { + md_http_next_req *nextreq; + void *baton; +} nextreq_proxy_t; + +static apr_status_t proxy_nextreq(md_http_request_t **preq, void *baton, + md_http_t *http, int in_flight) { - md_http_request_t *req; + nextreq_proxy_t *proxy = baton; apr_status_t rv; - rv = req_create(&req, http, "GET", url, headers, cb, baton); - if (rv != APR_SUCCESS) { - return rv; - } + rv = proxy->nextreq(preq, proxy->baton, http, in_flight); + if (APR_SUCCESS == rv) req_init_cl(*preq); + return rv; +} + +apr_status_t md_http_multi_perform(md_http_t *http, md_http_next_req *nextreq, void *baton) +{ + nextreq_proxy_t proxy; - return schedule(req, NULL, 0); + proxy.nextreq = nextreq; + proxy.baton = baton; + return http->impl->multi_perform(http, http->pool, proxy_nextreq, &proxy); } -apr_status_t md_http_HEAD(struct md_http_t *http, - const char *url, struct apr_table_t *headers, - md_http_cb *cb, void *baton) +apr_status_t md_http_GET_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers) { md_http_request_t *req; apr_status_t rv; - rv = req_create(&req, http, "HEAD", url, headers, cb, baton); - if (rv != APR_SUCCESS) { - return rv; - } - - return schedule(req, NULL, 0); + rv = req_create(&req, http, "GET", url, headers); + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; } -apr_status_t md_http_POST(struct md_http_t *http, const char *url, - struct apr_table_t *headers, const char *content_type, - apr_bucket_brigade *body, - md_http_cb *cb, void *baton) +apr_status_t md_http_HEAD_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers) { md_http_request_t *req; apr_status_t rv; - rv = req_create(&req, http, "POST", url, headers, cb, baton); - if (rv != APR_SUCCESS) { - return rv; - } - - if (content_type) { - apr_table_set(req->headers, "Content-Type", content_type); - } - return schedule(req, body, 1); + rv = req_create(&req, http, "HEAD", url, headers); + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; } -apr_status_t md_http_POSTd(md_http_t *http, const char *url, - struct apr_table_t *headers, const char *content_type, - const char *data, size_t data_len, - md_http_cb *cb, void *baton) +apr_status_t md_http_POST_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, int detect_len) { md_http_request_t *req; apr_status_t rv; - apr_bucket_brigade *body = NULL; - rv = req_create(&req, http, "POST", url, headers, cb, baton); - if (rv != APR_SUCCESS) { - return rv; + rv = req_create(&req, http, "POST", url, headers); + if (APR_SUCCESS == rv) { + rv = req_set_body(req, content_type, body, -1, detect_len); } + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; +} - if (data && data_len > 0) { - body = apr_brigade_create(req->pool, req->http->bucket_alloc); - rv = apr_brigade_write(body, NULL, NULL, data, data_len); - if (rv != APR_SUCCESS) { - md_http_req_destroy(req); - return rv; - } - } +apr_status_t md_http_POSTd_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const struct md_data_t *body) +{ + md_http_request_t *req; + apr_status_t rv; - if (content_type) { - apr_table_set(req->headers, "Content-Type", content_type); + rv = req_create(&req, http, "POST", url, headers); + if (APR_SUCCESS == rv) { + rv = req_set_body_data(req, content_type, body); } - - return schedule(req, body, 1); + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; +} + +apr_status_t md_http_GET_perform(struct md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_response_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_GET_create(&req, http, url, headers); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; +} + +apr_status_t md_http_HEAD_perform(struct md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_response_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_HEAD_create(&req, http, url, headers); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; +} + +apr_status_t md_http_POST_perform(struct md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + apr_bucket_brigade *body, int detect_len, + md_http_response_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_POST_create(&req, http, url, headers, content_type, body, detect_len); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; +} + +apr_status_t md_http_POSTd_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const md_data_t *body, + md_http_response_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_POSTd_create(&req, http, url, headers, content_type, body); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; } diff --git a/modules/md/md_http.h b/modules/md/md_http.h index 47c7cc4b57..23187b6dfa 100644 --- a/modules/md/md_http.h +++ b/modules/md/md_http.h @@ -20,17 +20,45 @@ struct apr_table_t; struct apr_bucket_brigade; struct apr_bucket_alloc_t; +struct md_data_t; typedef struct md_http_t md_http_t; typedef struct md_http_request_t md_http_request_t; typedef struct md_http_response_t md_http_response_t; -typedef apr_status_t md_http_cb(const md_http_response_t *res); +/** + * Callback invoked once per request, either when an error was encountered + * or when everything succeeded and the request is about to be released. Only + * in the last case will the status be APR_SUCCESS. + */ +typedef apr_status_t md_http_status_cb(const md_http_request_t *req, apr_status_t status, void *data); + +/** + * Callback invoked when the complete response has been received. + */ +typedef apr_status_t md_http_response_cb(const md_http_response_t *res, void *data); + +typedef struct md_http_callbacks_t md_http_callbacks_t; +struct md_http_callbacks_t { + md_http_status_cb *on_status; + void *on_status_data; + md_http_response_cb *on_response; + void *on_response_data; +}; + +typedef struct md_http_timeouts_t md_http_timeouts_t; +struct md_http_timeouts_t { + apr_time_t overall; + apr_time_t connect; + long stall_bytes_per_sec; + apr_time_t stalled; +}; struct md_http_request_t { md_http_t *http; apr_pool_t *pool; + int id; struct apr_bucket_alloc_t *bucket_alloc; const char *method; const char *url; @@ -40,14 +68,13 @@ struct md_http_request_t { struct apr_bucket_brigade *body; apr_off_t body_len; apr_off_t resp_limit; - md_http_cb *cb; - void *baton; + md_http_timeouts_t timeout; + md_http_callbacks_t cb; void *internals; }; struct md_http_response_t { md_http_request_t *req; - apr_status_t rv; int status; apr_table_t *headers; struct apr_bucket_brigade *body; @@ -58,38 +85,154 @@ apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_a void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit); -apr_status_t md_http_GET(md_http_t *http, - const char *url, struct apr_table_t *headers, - md_http_cb *cb, void *baton); +/** + * Set the timeout for the complete reqest. This needs to take everything from + * DNS looksups, to conntects, to transfer of all data into account and should + * be sufficiently large. + * Set to 0 the have no timeout for this. + */ +void md_http_set_timeout_default(md_http_t *http, apr_time_t timeout); +void md_http_set_timeout(md_http_request_t *req, apr_time_t timeout); + +/** + * Set the timeout for establishing a connection. + * Set to 0 the have no special timeout for this. + */ +void md_http_set_connect_timeout_default(md_http_t *http, apr_time_t timeout); +void md_http_set_connect_timeout(md_http_request_t *req, apr_time_t timeout); -apr_status_t md_http_HEAD(md_http_t *http, - const char *url, struct apr_table_t *headers, - md_http_cb *cb, void *baton); +/** + * Set the condition for when a transfer is considered "stalled", e.g. does not + * progress at a sufficient rate and will be aborted. + * Set to 0 the have no stall detection in place. + */ +void md_http_set_stalling_default(md_http_t *http, long bytes_per_sec, apr_time_t timeout); +void md_http_set_stalling(md_http_request_t *req, long bytes_per_sec, apr_time_t timeout); + +/** + * Perform the request. Then this function returns, the request and + * all its memory has been freed and must no longer be used. + */ +apr_status_t md_http_perform(md_http_request_t *request); -apr_status_t md_http_POST(md_http_t *http, const char *url, - struct apr_table_t *headers, const char *content_type, - struct apr_bucket_brigade *body, - md_http_cb *cb, void *baton); +/** + * Set the callback to be invoked once the status of a request is known. + * @param req the request + * @param cb the callback to invoke on the response + * @param baton data passed to the callback + */ +void md_http_set_on_status_cb(md_http_request_t *req, md_http_status_cb *cb, void *baton); + +/** + * Set the callback to be invoked when the complete + * response has been successfully received. The HTTP status may + * be 500, however. + * @param req the request + * @param cb the callback to invoke on the response + * @param baton data passed to the callback + */ +void md_http_set_on_response_cb(md_http_request_t *req, md_http_response_cb *cb, void *baton); + +/** + * Create a GET reqest. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + */ +apr_status_t md_http_GET_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers); + +/** + * Create a HEAD reqest. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + */ +apr_status_t md_http_HEAD_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers); + +/** + * Create a POST reqest with a bucket brigade as request body. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + * @param content_type the content_type of the body or NULL + * @param body the body of the request or NULL + * @param detect_len scan the body to detect its length + */ +apr_status_t md_http_POST_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, int detect_len); + +/** + * Create a POST reqest with known request body data. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + * @param content_type the content_type of the body or NULL + * @param body the body of the request or NULL + */ +apr_status_t md_http_POSTd_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const struct md_data_t *body); -apr_status_t md_http_POSTd(md_http_t *http, const char *url, - struct apr_table_t *headers, const char *content_type, - const char *data, size_t data_len, - md_http_cb *cb, void *baton); +/* + * Convenience functions for create+perform. + */ +apr_status_t md_http_GET_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, + md_http_response_cb *cb, void *baton); +apr_status_t md_http_HEAD_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, + md_http_response_cb *cb, void *baton); +apr_status_t md_http_POST_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, int detect_len, + md_http_response_cb *cb, void *baton); +apr_status_t md_http_POSTd_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const struct md_data_t *body, + md_http_response_cb *cb, void *baton); void md_http_req_destroy(md_http_request_t *req); +/** Return the next request for processing on APR_SUCCESS. Return ARP_ENOENT + * when no request is available. Anything else is an error. + */ +typedef apr_status_t md_http_next_req(md_http_request_t **preq, void *baton, + md_http_t *http, int in_flight); + +/** + * Perform requests in parallel as retrieved from the nextreq function. + * There are as many requests in flight as the nextreq functions provides. + * + * To limit the number of parallel requests, nextreq should return APR_ENOENT when the limit + * is reached. It will be called again when the number of in_flight requests changes. + * + * When all reqests are done, nextreq will be called one more time. Should it not + * return anything, this function returns. + */ +apr_status_t md_http_multi_perform(md_http_t *http, md_http_next_req *nextreq, void *baton); + /**************************************************************************************************/ /* interface to implementation */ typedef apr_status_t md_http_init_cb(void); typedef void md_http_req_cleanup_cb(md_http_request_t *req); typedef apr_status_t md_http_perform_cb(md_http_request_t *req); +typedef apr_status_t md_http_multi_perform_cb(md_http_t *http, apr_pool_t *p, + md_http_next_req *nextreq, void *baton); typedef struct md_http_impl_t md_http_impl_t; struct md_http_impl_t { md_http_init_cb *init; md_http_req_cleanup_cb *req_cleanup; md_http_perform_cb *perform; + md_http_multi_perform_cb *multi_perform; }; void md_http_use_implementation(md_http_impl_t *impl); diff --git a/modules/md/md_json.c b/modules/md/md_json.c index 5665479565..73120d78ed 100644 --- a/modules/md/md_json.c +++ b/modules/md/md_json.c @@ -18,10 +18,12 @@ #include <apr_lib.h> #include <apr_strings.h> #include <apr_buckets.h> +#include <apr_date.h> #include "md_json.h" #include "md_log.h" #include "md_http.h" +#include "md_time.h" #include "md_util.h" /* jansson thinks everyone compiles with the platform's cc in its fullest capabilities @@ -106,12 +108,12 @@ void md_json_destroy(md_json_t *json) } } -md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json) +md_json_t *md_json_copy(apr_pool_t *pool, const md_json_t *json) { return json_create(pool, json_copy(json->j)); } -md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json) +md_json_t *md_json_clone(apr_pool_t *pool, const md_json_t *json) { return json_create(pool, json_deep_copy(json->j)); } @@ -168,18 +170,16 @@ static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap) j = jselect_parent(&key, 1, json, ap); if (!j || !json_is_object(j)) { - json_decref(val); return APR_EINVAL; } aj = json_object_get(j, key); if (!aj) { aj = json_array(); - json_object_set_new(j, key, aj); + json_object_set(j, key, aj); } if (!json_is_array(aj)) { - json_decref(val); return APR_EINVAL; } @@ -202,7 +202,7 @@ static apr_status_t jselect_insert(json_t *val, size_t index, md_json_t *json, v aj = json_object_get(j, key); if (!aj) { aj = json_array(); - json_object_set_new(j, key, aj); + json_object_set(j, key, aj); } if (!json_is_array(aj)) { @@ -227,13 +227,11 @@ static apr_status_t jselect_set(json_t *val, md_json_t *json, va_list ap) j = jselect_parent(&key, 1, json, ap); if (!j) { - json_decref(val); return APR_EINVAL; } if (key) { if (!json_is_object(j)) { - json_decref(val); return APR_EINVAL; } json_object_set(j, key, val); @@ -313,6 +311,19 @@ int md_json_is(const md_json_type_t jtype, md_json_t *json, ...) return 0; } +static const char *md_json_type_name(const md_json_t *json) +{ + json_t *j = json->j; + if (json_is_object(j)) return "object"; + if (json_is_array(j)) return "array"; + if (json_is_string(j)) return "string"; + if (json_is_real(j)) return "real"; + if (json_is_integer(j)) return "integer"; + if (json_is_true(j)) return "true"; + if (json_is_false(j)) return "false"; + return "unknown"; +} + /**************************************************************************************************/ /* booleans */ @@ -428,6 +439,35 @@ apr_status_t md_json_sets(const char *value, md_json_t *json, ...) } /**************************************************************************************************/ +/* time */ + +apr_time_t md_json_get_time(const md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_string(j)) return 0; + return apr_date_parse_rfc(json_string_value(j)); +} + +apr_status_t md_json_set_time(apr_time_t value, md_json_t *json, ...) +{ + char ts[APR_RFC822_DATE_LEN]; + va_list ap; + apr_status_t rv; + + apr_rfc822_date(ts, value); + va_start(ap, json); + rv = jselect_set_new(json_string(ts), json, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ /* json itself */ md_json_t *md_json_getj(md_json_t *json, ...) @@ -449,6 +489,22 @@ md_json_t *md_json_getj(md_json_t *json, ...) return NULL; } +md_json_t *md_json_dupj(apr_pool_t *p, const md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j) { + json_incref(j); + return json_create(p, j); + } + return NULL; +} + const md_json_t *md_json_getcj(const md_json_t *json, ...) { json_t *j; @@ -468,7 +524,7 @@ const md_json_t *md_json_getcj(const md_json_t *json, ...) return NULL; } -apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) +apr_status_t md_json_setj(const md_json_t *value, md_json_t *json, ...) { va_list ap; apr_status_t rv; @@ -496,7 +552,7 @@ apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) return rv; } -apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...) +apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...) { va_list ap; apr_status_t rv; @@ -518,6 +574,25 @@ apr_status_t md_json_insertj(md_json_t *value, size_t index, md_json_t *json, .. return rv; } +apr_size_t md_json_limita(size_t max_elements, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + apr_size_t n = 0; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j && json_is_array(j)) { + n = json_array_size(j); + while (n > max_elements) { + json_array_remove(j, n-1); + n = json_array_size(j); + } + } + return n; +} /**************************************************************************************************/ /* arrays / objects */ @@ -642,7 +717,7 @@ apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void return md_json_setj(md_json_clone(p, value), json, NULL); } -apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) +apr_status_t md_json_clone_from(void **pvalue, const md_json_t *json, apr_pool_t *p, void *baton) { (void)baton; *pvalue = md_json_clone(p, json); @@ -843,7 +918,7 @@ apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...) /* formatting, parsing */ typedef struct { - md_json_t *json; + const md_json_t *json; md_json_fmt_t fmt; const char *fname; apr_file_t *f; @@ -868,7 +943,7 @@ static int dump_cb(const char *buffer, size_t len, void *baton) return (rv == APR_SUCCESS)? 0 : -1; } -apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb) +apr_status_t md_json_writeb(const md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb) { int rv = json_dump_callback(json->j, dump_cb, bb, fmt_to_flags(fmt)); return rv? APR_EGENERAL : APR_SUCCESS; @@ -877,14 +952,18 @@ apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, apr_bucket_briga static int chunk_cb(const char *buffer, size_t len, void *baton) { apr_array_header_t *chunks = baton; - char *chunk = apr_pcalloc(chunks->pool, len+1); + char *chunk; - memcpy(chunk, buffer, len); - APR_ARRAY_PUSH(chunks, const char *) = chunk; + if (len > 0) { + chunk = apr_palloc(chunks->pool, len+1); + memcpy(chunk, buffer, len); + chunk[len] = '\0'; + APR_ARRAY_PUSH(chunks, const char*) = chunk; + } return 0; } -const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) +const char *md_json_writep(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) { apr_array_header_t *chunks; int rv; @@ -901,13 +980,13 @@ const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) case 0: return ""; case 1: - return APR_ARRAY_IDX(chunks, 0, const char *); + return APR_ARRAY_IDX(chunks, 0, const char*); default: return apr_array_pstrcat(p, chunks, 0); } } -apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f) +apr_status_t md_json_writef(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f) { apr_status_t rv; const char *s; @@ -920,12 +999,13 @@ apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, a } else { rv = APR_EINVAL; - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef: error dumping json"); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, + "md_json_writef: error dumping json (%s)", md_json_dump_state(json, p)); } return rv; } -apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_fcreatex(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms) { apr_status_t rv; @@ -949,7 +1029,7 @@ static apr_status_t write_json(void *baton, apr_file_t *f, apr_pool_t *p) return rv; } -apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_freplace(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms) { j_write_ctx ctx; @@ -1076,11 +1156,9 @@ apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *p, const char *fpath) apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, const md_http_response_t *res) { apr_status_t rv = APR_ENOENT; - if (res->rv == APR_SUCCESS) { - const char *ctype = apr_table_get(res->headers, "content-type"); - if (ctype && res->body && (strstr(ctype, "/json") || strstr(ctype, "+json"))) { - rv = md_json_readb(pjson, pool, res->body); - } + const char *ctype = apr_table_get(res->headers, "content-type"); + if (ctype && res->body && (strstr(ctype, "/json") || strstr(ctype, "+json"))) { + rv = md_json_readb(pjson, pool, res->body); } return rv; } @@ -1091,9 +1169,9 @@ typedef struct { md_json_t *json; } resp_data; -static apr_status_t json_resp_cb(const md_http_response_t *res) +static apr_status_t json_resp_cb(const md_http_response_t *res, void *data) { - resp_data *resp = res->req->baton; + resp_data *resp = data; return md_json_read_http(&resp->json, resp->pool, res); } @@ -1106,7 +1184,7 @@ apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool, memset(&resp, 0, sizeof(resp)); resp.pool = pool; - rv = md_http_GET(http, url, NULL, json_resp_cb, &resp); + rv = md_http_GET_perform(http, url, NULL, json_resp_cb, &resp); if (rv == APR_SUCCESS) { *pjson = resp.json; @@ -1134,3 +1212,63 @@ apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...) } return rv; } + +const char *md_json_dump_state(const md_json_t *json, apr_pool_t *p) +{ + if (!json) return "NULL"; + return apr_psprintf(p, "%s, refc=%ld", md_json_type_name(json), (long)json->j->refcount); +} + +apr_status_t md_json_set_timeperiod(const md_timeperiod_t *tp, md_json_t *json, ...) +{ + char ts[APR_RFC822_DATE_LEN]; + json_t *jn, *j; + va_list ap; + const char *key; + apr_status_t rv; + + if (!tp || tp->start || tp->end) { + jn = json_object(); + apr_rfc822_date(ts, tp->start); + json_object_set_new(jn, "from", json_string(ts)); + apr_rfc822_date(ts, tp->end); + json_object_set_new(jn, "until", json_string(ts)); + + va_start(ap, json); + rv = jselect_set_new(jn, json, ap); + va_end(ap); + return rv; + } + else { + va_start(ap, json); + j = jselect_parent(&key, 0, json, ap); + va_end(ap); + + if (key && j && json_is_object(j)) { + json_object_del(j, key); + } + return APR_SUCCESS; + } +} + +apr_status_t md_json_get_timeperiod(md_timeperiod_t *tp, md_json_t *json, ...) +{ + json_t *j, *jts; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + memset(tp, 0, sizeof(*tp)); + if (!j) goto not_found; + jts = json_object_get(j, "from"); + if (!jts || !json_is_string(jts)) goto not_found; + tp->start = apr_date_parse_rfc(json_string_value(jts)); + jts = json_object_get(j, "until"); + if (!jts || !json_is_string(jts)) goto not_found; + tp->end = apr_date_parse_rfc(json_string_value(jts)); + return APR_SUCCESS; +not_found: + return APR_ENOENT; +} diff --git a/modules/md/md_json.h b/modules/md/md_json.h index 480ab7ca56..95a828b351 100644 --- a/modules/md/md_json.h +++ b/modules/md/md_json.h @@ -24,7 +24,7 @@ struct apr_file_t; struct md_http_t; struct md_http_response_t; - +struct md_timeperiod_t; typedef struct md_json_t md_json_t; @@ -47,8 +47,8 @@ typedef enum { md_json_t *md_json_create(apr_pool_t *pool); void md_json_destroy(md_json_t *json); -md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json); -md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json); +md_json_t *md_json_copy(apr_pool_t *pool, const md_json_t *json); +md_json_t *md_json_clone(apr_pool_t *pool, const md_json_t *json); int md_json_has_key(const md_json_t *json, ...); @@ -72,17 +72,25 @@ const char *md_json_gets(const md_json_t *json, ...); const char *md_json_dups(apr_pool_t *p, const md_json_t *json, ...); apr_status_t md_json_sets(const char *s, md_json_t *json, ...); +/* timestamp manipulation */ +apr_time_t md_json_get_time(const md_json_t *json, ...); +apr_status_t md_json_set_time(apr_time_t value, md_json_t *json, ...); + /* json manipulation */ md_json_t *md_json_getj(md_json_t *json, ...); +md_json_t *md_json_dupj(apr_pool_t *p, const md_json_t *json, ...); const md_json_t *md_json_getcj(const md_json_t *json, ...); -apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...); -apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_setj(const md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...); apr_status_t md_json_insertj(md_json_t *value, size_t index, md_json_t *json, ...); /* Array/Object manipulation */ apr_status_t md_json_clr(md_json_t *json, ...); apr_status_t md_json_del(md_json_t *json, ...); +/* Remove all array elements beyond max_elements */ +apr_size_t md_json_limita(size_t max_elements, md_json_t *json, ...); + /* conversion function from and to json */ typedef apr_status_t md_json_to_cb(void *value, md_json_t *json, apr_pool_t *p, void *baton); typedef apr_status_t md_json_from_cb(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton); @@ -93,7 +101,7 @@ apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, vo /* conversions from json to json in specified pool */ apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton); -apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton); +apr_status_t md_json_clone_from(void **pvalue, const md_json_t *json, apr_pool_t *p, void *baton); /* Manipulating/Iteration on generic Arrays */ apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, @@ -115,13 +123,13 @@ apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...); /* serialization & parsing */ -apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); -const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); -apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, +apr_status_t md_json_writeb(const md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); +const char *md_json_writep(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); +apr_status_t md_json_writef(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, struct apr_file_t *f); -apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_fcreatex(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms); -apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_freplace(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms); apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, struct apr_bucket_brigade *bb); @@ -137,4 +145,9 @@ apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...); +const char *md_json_dump_state(const md_json_t *json, apr_pool_t *p); + +apr_status_t md_json_set_timeperiod(const struct md_timeperiod_t *tp, md_json_t *json, ...); +apr_status_t md_json_get_timeperiod(struct md_timeperiod_t *tp, md_json_t *json, ...); + #endif /* md_json_h */ diff --git a/modules/md/md_jws.c b/modules/md/md_jws.c index 810c151c2f..cfbe8da89b 100644 --- a/modules/md/md_jws.c +++ b/modules/md/md_jws.c @@ -32,13 +32,13 @@ static int header_set(void *data, const char *key, const char *val) } apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, - const char *payload, size_t len, - struct apr_table_t *protected, + md_data_t *payload, struct apr_table_t *protected, struct md_pkey_t *pkey, const char *key_id) { md_json_t *msg, *jprotected; const char *prot64, *pay64, *sign64, *sign, *prot; apr_status_t rv = APR_SUCCESS; + md_data_t data; *pmsg = NULL; @@ -64,9 +64,11 @@ apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, } if (rv == APR_SUCCESS) { - prot64 = md_util_base64url_encode(prot, strlen(prot), p); + data.data = prot; + data.len = strlen(prot); + prot64 = md_util_base64url_encode(&data, p); md_json_sets(prot64, msg, "protected", NULL); - pay64 = md_util_base64url_encode(payload, len, p); + pay64 = md_util_base64url_encode(payload, p); md_json_sets(pay64, msg, "payload", NULL); sign = apr_psprintf(p, "%s.%s", prot64, pay64); @@ -91,7 +93,7 @@ apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey) { const char *e64, *n64, *s; - md_data data; + md_data_t data; apr_status_t rv; e64 = md_pkey_get_rsa_e64(pkey, p); diff --git a/modules/md/md_jws.h b/modules/md/md_jws.h index e7c145ee38..7121308aae 100644 --- a/modules/md/md_jws.h +++ b/modules/md/md_jws.h @@ -20,9 +20,10 @@ struct apr_table_t; struct md_json_t; struct md_pkey_t; +struct md_data_t; apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, - const char *payload, size_t len, struct apr_table_t *protected, + struct md_data_t *payload, struct apr_table_t *protected, struct md_pkey_t *pkey, const char *key_id); apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey); diff --git a/modules/md/md_ocsp.c b/modules/md/md_ocsp.c new file mode 100644 index 0000000000..90fb332905 --- /dev/null +++ b/modules/md/md_ocsp.c @@ -0,0 +1,1037 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_buckets.h> +#include <apr_hash.h> +#include <apr_time.h> +#include <apr_date.h> +#include <apr_strings.h> +#include <apr_thread_mutex.h> + +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/ocsp.h> +#include <openssl/pem.h> +#include <openssl/x509v3.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_log.h" +#include "md_http.h" +#include "md_json.h" +#include "md_result.h" +#include "md_status.h" +#include "md_store.h" +#include "md_util.h" +#include "md_ocsp.h" + +#define MD_OCSP_ID_LENGTH SHA_DIGEST_LENGTH + +struct md_ocsp_reg_t { + apr_pool_t *p; + md_store_t *store; + const char *user_agent; + const char *proxy_url; + apr_hash_t *hash; + apr_thread_mutex_t *mutex; + md_timeslice_t renew_window; + md_job_notify_cb *notify; + void *notify_ctx; +}; + +typedef struct md_ocsp_status_t md_ocsp_status_t; +struct md_ocsp_status_t { + md_data_t id; + const char *hexid; + const char *hex_sha256; + OCSP_CERTID *certid; + const char *responder_url; + + apr_time_t next_run; /* when the responder shall be asked again */ + int errors; /* consecutive failed attempts */ + + md_ocsp_cert_stat_t resp_stat; + md_data_t resp_der; + md_timeperiod_t resp_valid; + + md_data_t req_der; + OCSP_REQUEST *ocsp_req; + md_ocsp_reg_t *reg; + + const char *md_name; + const char *file_name; + + apr_time_t resp_mtime; + apr_time_t resp_last_check; +}; + +const char *md_ocsp_cert_stat_name(md_ocsp_cert_stat_t stat) +{ + switch (stat) { + case MD_OCSP_CERT_ST_GOOD: return "good"; + case MD_OCSP_CERT_ST_REVOKED: return "revoked"; + default: return "unknown"; + } +} + +md_ocsp_cert_stat_t md_ocsp_cert_stat_value(const char *name) +{ + if (name && !strcmp("good", name)) return MD_OCSP_CERT_ST_GOOD; + if (name && !strcmp("revoked", name)) return MD_OCSP_CERT_ST_REVOKED; + return MD_OCSP_CERT_ST_UNKNOWN; +} + +static apr_status_t init_cert_id(md_data_t *data, const md_cert_t *cert) +{ + X509 *x = md_cert_get_X509(cert); + unsigned int ulen = 0; + + assert(data->len == SHA_DIGEST_LENGTH); + if (X509_digest(x, EVP_sha1(), (unsigned char*)data->data, &ulen) != 1) { + return APR_EGENERAL; + } + data->len = ulen; + return APR_SUCCESS; +} + +static void ostat_req_cleanup(md_ocsp_status_t *ostat) +{ + if (ostat->ocsp_req) { + OCSP_REQUEST_free(ostat->ocsp_req); + ostat->ocsp_req = NULL; + } + if (ostat->req_der.data) { + OPENSSL_free((void*)ostat->req_der.data); + ostat->req_der.data = NULL; + ostat->req_der.len = 0; + } +} + +static int ostat_cleanup(void *ctx, const void *key, apr_ssize_t klen, const void *val) +{ + md_ocsp_reg_t *reg = ctx; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + + (void)reg; + (void)key; + (void)klen; + ostat_req_cleanup(ostat); + if (ostat->certid) { + OCSP_CERTID_free(ostat->certid); + ostat->certid = NULL; + } + if (ostat->resp_der.data) { + OPENSSL_free((void*)ostat->resp_der.data); + ostat->resp_der.data = NULL; + ostat->resp_der.len = 0; + } + return 1; +} + +static int ostat_should_renew(md_ocsp_status_t *ostat) +{ + md_timeperiod_t renewal; + + renewal = md_timeperiod_slice_before_end(&ostat->resp_valid, &ostat->reg->renew_window); + return md_timeperiod_has_started(&renewal, apr_time_now()); +} + +static apr_status_t ostat_set(md_ocsp_status_t *ostat, md_ocsp_cert_stat_t stat, + md_data_t *der, md_timeperiod_t *valid, apr_time_t mtime) +{ + apr_status_t rv = APR_SUCCESS; + char *s = (char*)der->data; + + if (der->len) { + s = OPENSSL_malloc(der->len); + if (!s) { + rv = APR_ENOMEM; + goto leave; + } + memcpy((char*)s, der->data, der->len); + } + + if (ostat->resp_der.data) { + OPENSSL_free((void*)ostat->resp_der.data); + ostat->resp_der.data = NULL; + ostat->resp_der.len = 0; + } + + ostat->resp_stat = stat; + ostat->resp_der.data = s; + ostat->resp_der.len = der->len; + ostat->resp_valid = *valid; + ostat->resp_mtime = mtime; + + ostat->errors = 0; + ostat->next_run = md_timeperiod_slice_before_end( + &ostat->resp_valid, &ostat->reg->renew_window).start; + +leave: + return rv; +} + +static apr_status_t ostat_from_json(md_ocsp_cert_stat_t *pstat, + md_data_t *resp_der, md_timeperiod_t *resp_valid, + md_json_t *json, apr_pool_t *p) +{ + const char *s; + md_timeperiod_t valid; + apr_status_t rv = APR_ENOENT; + + memset(resp_der, 0, sizeof(*resp_der)); + memset(resp_valid, 0, sizeof(*resp_valid)); + s = md_json_dups(p, json, MD_KEY_VALID, MD_KEY_FROM, NULL); + if (s && *s) valid.start = apr_date_parse_rfc(s); + s = md_json_dups(p, json, MD_KEY_VALID, MD_KEY_UNTIL, NULL); + if (s && *s) valid.end = apr_date_parse_rfc(s); + s = md_json_dups(p, json, MD_KEY_RESPONSE, NULL); + if (!s || !*s) goto leave; + md_util_base64url_decode(resp_der, s, p); + *pstat = md_ocsp_cert_stat_value(md_json_gets(json, MD_KEY_STATUS, NULL)); + *resp_valid = valid; + rv = APR_SUCCESS; +leave: + return rv; +} + +static void ostat_to_json(md_json_t *json, md_ocsp_cert_stat_t stat, + const md_data_t *resp_der, const md_timeperiod_t *resp_valid, + apr_pool_t *p) +{ + const char *s = NULL; + + if (resp_der->len > 0) { + md_json_sets(md_util_base64url_encode(resp_der, p), json, MD_KEY_RESPONSE, NULL); + s = md_ocsp_cert_stat_name(stat); + if (s) md_json_sets(s, json, MD_KEY_STATUS, NULL); + md_json_set_timeperiod(resp_valid, json, MD_KEY_VALID, NULL); + } +} + +static apr_status_t ocsp_status_refresh(md_ocsp_status_t *ostat, apr_pool_t *ptemp) +{ + md_store_t *store = ostat->reg->store; + md_json_t *jprops; + apr_time_t mtime; + apr_status_t rv = APR_EAGAIN; + md_data_t resp_der; + md_timeperiod_t resp_valid; + md_ocsp_cert_stat_t resp_stat; + /* Check if the store holds a newer response than the one we have */ + mtime = md_store_get_modified(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, ptemp); + if (mtime <= ostat->resp_mtime) goto leave; + rv = md_store_load_json(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, &jprops, ptemp); + if (APR_SUCCESS != rv) goto leave; + rv = ostat_from_json(&resp_stat, &resp_der, &resp_valid, jprops, ptemp); + if (APR_SUCCESS != rv) goto leave; + rv = ostat_set(ostat, resp_stat, &resp_der, &resp_valid, mtime); + if (APR_SUCCESS != rv) goto leave; +leave: + return rv; +} + + +static apr_status_t ocsp_status_save(md_ocsp_cert_stat_t stat, const md_data_t *resp_der, + const md_timeperiod_t *resp_valid, + md_ocsp_status_t *ostat, apr_pool_t *ptemp) +{ + md_store_t *store = ostat->reg->store; + md_json_t *jprops; + apr_time_t mtime; + apr_status_t rv; + + jprops = md_json_create(ptemp); + ostat_to_json(jprops, stat, resp_der, resp_valid, ptemp); + rv = md_store_save_json(store, ptemp, MD_SG_OCSP, ostat->md_name, ostat->file_name, jprops, 0); + if (APR_SUCCESS != rv) goto leave; + mtime = md_store_get_modified(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, ptemp); + if (mtime) ostat->resp_mtime = mtime; +leave: + return rv; +} + +static apr_status_t ocsp_reg_cleanup(void *data) +{ + md_ocsp_reg_t *reg = data; + + /* free all OpenSSL structures that we hold */ + apr_hash_do(ostat_cleanup, reg, reg->hash); + return APR_SUCCESS; +} + +apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, md_store_t *store, + const md_timeslice_t *renew_window, + const char *user_agent, const char *proxy_url) +{ + md_ocsp_reg_t *reg; + apr_status_t rv = APR_SUCCESS; + + reg = apr_palloc(p, sizeof(*reg)); + if (!reg) { + rv = APR_ENOMEM; + goto leave; + } + reg->p = p; + reg->store = store; + reg->user_agent = user_agent; + reg->proxy_url = proxy_url; + reg->hash = apr_hash_make(p); + reg->renew_window = *renew_window; + + rv = apr_thread_mutex_create(®->mutex, APR_THREAD_MUTEX_NESTED, p); + if (APR_SUCCESS != rv) goto leave; + + apr_pool_cleanup_register(p, reg, ocsp_reg_cleanup, apr_pool_cleanup_null); +leave: + *preg = (APR_SUCCESS == rv)? reg : NULL; + return rv; +} + +apr_status_t md_ocsp_prime(md_ocsp_reg_t *reg, md_cert_t *cert, md_cert_t *issuer, const md_t *md) +{ + char iddata[MD_OCSP_ID_LENGTH]; + md_ocsp_status_t *ostat; + STACK_OF(OPENSSL_STRING) *ssk = NULL; + const char *name, *s; + md_data_t id; + apr_status_t rv; + + /* Called during post_config. no mutex protection needed */ + name = md? md->name : MD_OTHER; + id.data = iddata; id.len = sizeof(iddata); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, reg->p, + "md[%s]: priming OCSP status", name); + rv = init_cert_id(&id, cert); + if (APR_SUCCESS != rv) goto leave; + + ostat = apr_hash_get(reg->hash, id.data, (apr_ssize_t)id.len); + if (ostat) goto leave; /* already seen it, cert is used in >1 server_rec */ + + ostat = apr_pcalloc(reg->p, sizeof(*ostat)); + md_data_assign_pcopy(&ostat->id, &id, reg->p); + ostat->reg = reg; + ostat->md_name = name; + md_data_to_hex(&ostat->hexid, 0, reg->p, &ostat->id); + ostat->file_name = apr_psprintf(reg->p, "ocsp-%s.json", ostat->hexid); + rv = md_cert_to_sha256_fingerprint(&ostat->hex_sha256, cert, reg->p); + if (APR_SUCCESS != rv) goto leave; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: getting ocsp responder from cert", name); + ssk = X509_get1_ocsp(md_cert_get_X509(cert)); + if (!ssk) { + rv = APR_ENOENT; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, reg->p, + "md[%s]: certificate with serial %s has not OCSP responder URL", + name, md_cert_get_serial_number(cert, reg->p)); + goto leave; + } + s = sk_OPENSSL_STRING_value(ssk, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: ocsp responder found '%s'", name, s); + ostat->responder_url = apr_pstrdup(reg->p, s); + X509_email_free(ssk); + + ostat->certid = OCSP_cert_to_id(NULL, md_cert_get_X509(cert), md_cert_get_X509(issuer)); + if (!ostat->certid) { + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, reg->p, + "md[%s]: unable to create OCSP certid for certificate with serial %s", + name, md_cert_get_serial_number(cert, reg->p)); + goto leave; + } + + /* See, if we have something in store */ + ocsp_status_refresh(ostat, reg->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, reg->p, + "md[%s]: adding ocsp info (responder=%s)", + name, ostat->responder_url); + apr_hash_set(reg->hash, ostat->id.data, (apr_ssize_t)ostat->id.len, ostat); + rv = APR_SUCCESS; +leave: + return rv; +} + +apr_status_t md_ocsp_get_status(unsigned char **pder, int *pderlen, + md_ocsp_reg_t *reg, const md_cert_t *cert, + apr_pool_t *p, const md_t *md) +{ + char iddata[MD_OCSP_ID_LENGTH]; + md_ocsp_status_t *ostat; + const char *name; + apr_status_t rv; + int locked = 0; + md_data_t id; + + (void)p; + (void)md; + id.data = iddata; id.len = sizeof(iddata); + *pder = NULL; + *pderlen = 0; + name = md? md->name : MD_OTHER; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, get_status", name); + rv = init_cert_id(&id, cert); + if (APR_SUCCESS != rv) goto leave; + + ostat = apr_hash_get(reg->hash, id.data, (apr_ssize_t)id.len); + if (!ostat) { + rv = APR_ENOENT; + goto leave; + } + + /* While the ostat instance itself always exists, the response data it holds + * may vary over time and we need locked access to make a copy. */ + apr_thread_mutex_lock(reg->mutex); + locked = 1; + + if (ostat->resp_der.len <= 0) { + /* No response known, check store for new response. */ + ocsp_status_refresh(ostat, p); + if (ostat->resp_der.len <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, no response available", name); + goto leave; + } + } + /* We have a response */ + if (ostat_should_renew(ostat)) { + /* But it is up for renewal. A watchdog should be busy with + * retrieving a new one. In case of outages, this might take + * a while, however. Pace the frequency of checks with the + * urgency of a new response based on the remaining time. */ + long secs = (long)apr_time_sec(md_timeperiod_remaining(&ostat->resp_valid, apr_time_now())); + apr_time_t waiting_time; + + /* every hour, every minute, every second */ + waiting_time = ((secs >= MD_SECS_PER_DAY)? + apr_time_from_sec(60 * 60) : ((secs >= 60)? + apr_time_from_sec(60) : apr_time_from_sec(1))); + if ((apr_time_now() - ostat->resp_last_check) >= waiting_time) { + ostat->resp_last_check = apr_time_now(); + ocsp_status_refresh(ostat, p); + } + } + + *pder = OPENSSL_malloc(ostat->resp_der.len); + if (*pder == NULL) { + rv = APR_ENOMEM; + goto leave; + } + memcpy(*pder, ostat->resp_der.data, ostat->resp_der.len); + *pderlen = (int)ostat->resp_der.len; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, returning %ld bytes of response", + name, (long)ostat->resp_der.len); +leave: + if (locked) apr_thread_mutex_unlock(reg->mutex); + return rv; +} + +static void ocsp_get_meta(md_ocsp_cert_stat_t *pstat, md_timeperiod_t *pvalid, + md_ocsp_reg_t *reg, md_ocsp_status_t *ostat, apr_pool_t *p) +{ + apr_thread_mutex_lock(reg->mutex); + if (ostat->resp_der.len <= 0) { + /* No resonse known, check the store if out watchdog retrieved one + * in the meantime. */ + ocsp_status_refresh(ostat, p); + } + *pvalid = ostat->resp_valid; + *pstat = ostat->resp_stat; + apr_thread_mutex_unlock(reg->mutex); +} + +apr_status_t md_ocsp_get_meta(md_ocsp_cert_stat_t *pstat, md_timeperiod_t *pvalid, + md_ocsp_reg_t *reg, const md_cert_t *cert, + apr_pool_t *p, const md_t *md) +{ + char iddata[MD_OCSP_ID_LENGTH]; + md_ocsp_status_t *ostat; + const char *name; + apr_status_t rv; + md_timeperiod_t valid; + md_ocsp_cert_stat_t stat; + md_data_t id; + + (void)p; + (void)md; + id.data = iddata; id.len = sizeof(iddata); + name = md? md->name : MD_OTHER; + memset(&valid, 0, sizeof(valid)); + stat = MD_OCSP_CERT_ST_UNKNOWN; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, get_status", name); + + rv = init_cert_id(&id, cert); + if (APR_SUCCESS != rv) goto leave; + + ostat = apr_hash_get(reg->hash, id.data, (apr_ssize_t)id.len); + if (!ostat) { + rv = APR_ENOENT; + goto leave; + } + ocsp_get_meta(&stat, &valid, reg, ostat, p); +leave: + *pstat = stat; + *pvalid = valid; + return rv; +} + +apr_size_t md_ocsp_count(md_ocsp_reg_t *reg) +{ + return apr_hash_count(reg->hash); +} + +static const char *certid_as_hex(const OCSP_CERTID *certid, apr_pool_t *p) +{ + md_data_t der; + const char *hex; + + memset(&der, 0, sizeof(der)); + der.len = (apr_size_t)i2d_OCSP_CERTID((OCSP_CERTID*)certid, (unsigned char**)&der.data); + md_data_to_hex(&hex, 0, p, &der); + OPENSSL_free((void*)der.data); + return hex; +} + +static const char *certid_summary(const OCSP_CERTID *certid, apr_pool_t *p) +{ + const char *serial, *issuer, *key, *s; + ASN1_INTEGER *aserial; + ASN1_OCTET_STRING *aname_hash, *akey_hash; + ASN1_OBJECT *amd_nid; + BIGNUM *bn; + md_data_t data; + + serial = issuer = key = "???"; + OCSP_id_get0_info(&aname_hash, &amd_nid, &akey_hash, &aserial, (OCSP_CERTID*)certid); + if (aname_hash) { + data.len = (apr_size_t)aname_hash->length; + data.data = (const char*)aname_hash->data; + md_data_to_hex(&issuer, 0, p, &data); + } + if (akey_hash) { + data.len = (apr_size_t)akey_hash->length; + data.data = (const char*)akey_hash->data; + md_data_to_hex(&key, 0, p, &data); + } + if (aserial) { + bn = ASN1_INTEGER_to_BN(aserial, NULL); + s = BN_bn2hex(bn); + serial = apr_pstrdup(p, s); + OPENSSL_free((void*)bn); + OPENSSL_free((void*)s); + } + return apr_psprintf(p, "certid[der=%s, issuer=%s, key=%s, serial=%s]", + certid_as_hex(certid, p), issuer, key, serial); +} + +static const char *certstatus_string(int status) +{ + switch (status) { + case V_OCSP_CERTSTATUS_GOOD: return "good"; + case V_OCSP_CERTSTATUS_REVOKED: return "revoked"; + case V_OCSP_CERTSTATUS_UNKNOWN: return "unknown"; + default: return "???"; + } + +} + +static const char *single_resp_summary(OCSP_SINGLERESP* resp, apr_pool_t *p) +{ + const OCSP_CERTID *certid; + int status, reason = 0; + ASN1_GENERALIZEDTIME *bup = NULL, *bnextup = NULL; + md_timeperiod_t valid; + + certid = OCSP_SINGLERESP_get0_id(resp); + status = OCSP_single_get0_status(resp, &reason, NULL, &bup, &bnextup); + valid.start = bup? md_asn1_generalized_time_get(bup) : apr_time_now(); + valid.end = md_asn1_generalized_time_get(bnextup); + + return apr_psprintf(p, "ocsp-single-resp[%s, status=%s, reason=%d, valid=%s]", + certid_summary(certid, p), + certstatus_string(status), reason, + md_timeperiod_print(p, &valid)); +} + +typedef struct { + apr_pool_t *p; + md_ocsp_status_t *ostat; + md_result_t *result; + md_job_t *job; +} md_ocsp_update_t; + +static apr_status_t ostat_on_resp(const md_http_response_t *resp, void *baton) +{ + md_ocsp_update_t *update = baton; + md_ocsp_status_t *ostat = update->ostat; + md_http_request_t *req = resp->req; + OCSP_RESPONSE *ocsp_resp = NULL; + OCSP_BASICRESP *basic_resp = NULL; + OCSP_SINGLERESP *single_resp; + apr_status_t rv = APR_SUCCESS; + int n, breason = 0, bstatus; + ASN1_GENERALIZEDTIME *bup = NULL, *bnextup = NULL; + md_data_t der, new_der; + md_timeperiod_t valid; + md_ocsp_cert_stat_t nstat; + + der.data = new_der.data = NULL; + der.len = new_der.len = 0; + + md_result_activity_printf(update->result, "status of certid %s, reading response", + ostat->hexid); + if (APR_SUCCESS != (rv = apr_brigade_pflatten(resp->body, (char**)&der.data, + &der.len, req->pool))) { + goto leave; + } + if (NULL == (ocsp_resp = d2i_OCSP_RESPONSE(NULL, (const unsigned char**)&der.data, + (long)der.len))) { + rv = APR_EINVAL; + md_result_set(update->result, rv, "response body does not parse as OCSP response"); + md_result_log(update->result, MD_LOG_DEBUG); + goto leave; + } + /* got a response! but what does it say? */ + n = OCSP_response_status(ocsp_resp); + if (OCSP_RESPONSE_STATUS_SUCCESSFUL != n) { + rv = APR_EINVAL; + md_result_printf(update->result, rv, "OCSP response status is, unsuccessfully, %d", n); + md_result_log(update->result, MD_LOG_DEBUG); + goto leave; + } + basic_resp = OCSP_response_get1_basic(ocsp_resp); + if (!basic_resp) { + rv = APR_EINVAL; + md_result_set(update->result, rv, "OCSP response has no basicresponse"); + md_result_log(update->result, MD_LOG_DEBUG); + goto leave; + } + /* The notion of nonce enabled freshness in OCSP responses, e.g. that the response + * contains the signed nonce we sent to the responder, does not scale well. Responders + * like to return cached response bytes and therefore do not add a nonce to it. + * So, in reality, we can only detect a mismatch when present and otherwise have + * to accept it. */ + switch ((n = OCSP_check_nonce(ostat->ocsp_req, basic_resp))) { + case 1: + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->pool, + "req[%d]: OCSP respoonse nonce does match", req->id); + break; + case 0: + rv = APR_EINVAL; + md_result_printf(update->result, rv, "OCSP nonce mismatch in response", n); + md_result_log(update->result, MD_LOG_WARNING); + goto leave; + + case -1: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, + "req[%d]: OCSP respoonse did not return the nonce", req->id); + break; + default: + break; + } + + if (!OCSP_resp_find_status(basic_resp, ostat->certid, &bstatus, + &breason, NULL, &bup, &bnextup)) { + const char *prefix, *slist = "", *sep = ""; + int i; + + rv = APR_EINVAL; + prefix = apr_psprintf(req->pool, "OCSP response, no matching status reported for %s", + certid_summary(ostat->certid, req->pool)); + for (i = 0; i < OCSP_resp_count(basic_resp); ++i) { + single_resp = OCSP_resp_get0(basic_resp, i); + slist = apr_psprintf(req->pool, "%s%s%s", slist, sep, + single_resp_summary(single_resp, req->pool)); + sep = ", "; + } + md_result_printf(update->result, rv, "%s, status list [%s]", prefix, slist); + md_result_log(update->result, MD_LOG_DEBUG); + goto leave; + } + if (V_OCSP_CERTSTATUS_UNKNOWN == bstatus) { + rv = APR_ENOENT; + md_result_set(update->result, rv, "OCSP basicresponse says cert is unknown"); + md_result_log(update->result, MD_LOG_DEBUG); + goto leave; + } + if (!bnextup) { + rv = APR_EINVAL; + md_result_set(update->result, rv, "OCSP basicresponse reports not valid dates"); + md_result_log(update->result, MD_LOG_DEBUG); + goto leave; + } + + /* Coming here, we have a response for our certid and it is either GOOD + * or REVOKED. Both cases we want to remember and use in stapling. */ + n = i2d_OCSP_RESPONSE(ocsp_resp, (unsigned char**)&new_der.data); + if (n <= 0) { + rv = APR_EGENERAL; + md_result_set(update->result, rv, "error DER encoding OCSP response"); + md_result_log(update->result, MD_LOG_WARNING); + goto leave; + } + nstat = (bstatus == V_OCSP_CERTSTATUS_GOOD)? MD_OCSP_CERT_ST_GOOD : MD_OCSP_CERT_ST_REVOKED; + new_der.len = (apr_size_t)n; + valid.start = bup? md_asn1_generalized_time_get(bup) : apr_time_now(); + valid.end = md_asn1_generalized_time_get(bnextup); + + /* First, update the instance with a copy */ + apr_thread_mutex_lock(ostat->reg->mutex); + ostat_set(ostat, nstat, &new_der, &valid, apr_time_now()); + apr_thread_mutex_unlock(ostat->reg->mutex); + + /* Next, save the original response */ + rv = ocsp_status_save(nstat, &new_der, &valid, ostat, req->pool); + if (APR_SUCCESS != rv) { + md_result_set(update->result, rv, "error saving OCSP status"); + md_result_log(update->result, MD_LOG_ERR); + goto leave; + } + + md_result_printf(update->result, rv, "certificate status is %s, status valid %s", + (nstat == MD_OCSP_CERT_ST_GOOD)? "GOOD" : "REVOKED", + md_timeperiod_print(req->pool, &ostat->resp_valid)); + md_result_log(update->result, MD_LOG_DEBUG); + +leave: + if (new_der.data) OPENSSL_free((void*)new_der.data); + if (basic_resp) OCSP_BASICRESP_free(basic_resp); + if (ocsp_resp) OCSP_RESPONSE_free(ocsp_resp); + return rv; +} + +static apr_status_t ostat_on_req_status(const md_http_request_t *req, apr_status_t status, + void *baton) +{ + md_ocsp_update_t *update = baton; + md_ocsp_status_t *ostat = update->ostat; + + (void)req; + md_job_end_run(update->job, update->result); + if (APR_SUCCESS != status) { + ++ostat->errors; + ostat->next_run = apr_time_now() + md_job_delay_on_errors(ostat->errors); + md_result_printf(update->result, status, "OCSP status update failed (%d. time)", + ostat->errors); + md_result_log(update->result, MD_LOG_DEBUG); + md_job_log_append(update->job, "ocsp-error", + update->result->problem, update->result->detail); + md_job_holler(update->job, "ocsp-errored"); + goto leave; + } + md_job_notify(update->job, "ocsp-renewed", update->result); + +leave: + md_job_save(update->job, update->result, update->p); + ostat_req_cleanup(ostat); + return APR_SUCCESS; +} + +typedef struct { + md_ocsp_reg_t *reg; + apr_array_header_t *todos; + apr_pool_t *ptemp; + apr_time_t time; + int max_parallel; +} md_ocsp_todo_ctx_t; + +static apr_status_t next_todo(md_http_request_t **preq, void *baton, + md_http_t *http, int in_flight) +{ + md_ocsp_todo_ctx_t *ctx = baton; + md_ocsp_update_t *update, **pupdate; + md_ocsp_status_t *ostat; + OCSP_CERTID *certid = NULL; + md_http_request_t *req = NULL; + apr_status_t rv = APR_ENOENT; + apr_table_t *headers; + int len; + + if (in_flight < ctx->max_parallel) { + pupdate = apr_array_pop(ctx->todos); + if (pupdate) { + update = *pupdate; + ostat = update->ostat; + + update->job = md_ocsp_job_make(ctx->reg, ostat->md_name, update->p); + md_job_load(update->job); + md_job_start_run(update->job, update->result, ctx->reg->store); + + if (!ostat->ocsp_req) { + ostat->ocsp_req = OCSP_REQUEST_new(); + if (!ostat->ocsp_req) goto leave; + certid = OCSP_CERTID_dup(ostat->certid); + if (!certid) goto leave; + if (!OCSP_request_add0_id(ostat->ocsp_req, certid)) goto leave; + OCSP_request_add1_nonce(ostat->ocsp_req, 0, -1); + certid = NULL; + } + if (0 == ostat->req_der.len) { + len = i2d_OCSP_REQUEST(ostat->ocsp_req, (unsigned char**)&ostat->req_der.data); + if (len < 0) goto leave; + ostat->req_der.len = (apr_size_t)len; + } + md_result_activity_printf(update->result, "status of certid %s, " + "contacting %s", ostat->hexid, ostat->responder_url); + headers = apr_table_make(ctx->ptemp, 5); + apr_table_set(headers, "Expect", ""); + rv = md_http_POSTd_create(&req, http, ostat->responder_url, headers, + "application/ocsp-request", &ostat->req_der); + if (APR_SUCCESS != rv) goto leave; + md_http_set_on_status_cb(req, ostat_on_req_status, update); + md_http_set_on_response_cb(req, ostat_on_resp, update); + rv = APR_SUCCESS; + } + } +leave: + *preq = (APR_SUCCESS == rv)? req : NULL; + if (certid) OCSP_CERTID_free(certid); + return rv; +} + +static int select_updates(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + md_ocsp_todo_ctx_t *ctx = baton; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + md_ocsp_update_t *update; + + (void)key; + (void)klen; + if (ostat->next_run <= ctx->time) { + update = apr_pcalloc(ctx->ptemp, sizeof(*update)); + update->p = ctx->ptemp; + update->ostat = ostat; + update->result = md_result_md_make(update->p, ostat->md_name); + update->job = NULL; + APR_ARRAY_PUSH(ctx->todos, md_ocsp_update_t*) = update; + } + return 1; +} + +static int select_next_run(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + md_ocsp_todo_ctx_t *ctx = baton; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + + (void)key; + (void)klen; + if (ostat->next_run < ctx->time && ostat->next_run > apr_time_now()) { + ctx->time = ostat->next_run; + } + return 1; +} + +void md_ocsp_renew(md_ocsp_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, apr_time_t *pnext_run) +{ + md_ocsp_todo_ctx_t ctx; + md_http_t *http; + apr_status_t rv = APR_SUCCESS; + + (void)p; + (void)pnext_run; + + ctx.reg = reg; + ctx.ptemp = ptemp; + ctx.todos = apr_array_make(ptemp, (int)md_ocsp_count(reg), sizeof(md_ocsp_status_t*)); + ctx.max_parallel = 6; /* the magic number in HTTP */ + + /* Create a list of update tasks that are needed now or in the next minute */ + ctx.time = apr_time_now() + apr_time_from_sec(60);; + apr_hash_do(select_updates, &ctx, reg->hash); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "OCSP status updates due: %d", ctx.todos->nelts); + if (!ctx.todos->nelts) goto leave; + + rv = md_http_create(&http, ptemp, reg->user_agent, reg->proxy_url); + if (APR_SUCCESS != rv) goto leave; + + rv = md_http_multi_perform(http, next_todo, &ctx); + +leave: + /* When do we need to run next? *pnext_run contains the planned schedule from + * the watchdog. We can make that earlier if we need it. */ + ctx.time = *pnext_run; + apr_hash_do(select_next_run, &ctx, reg->hash); + + /* sanity check and return */ + if (ctx.time < apr_time_now()) ctx.time = apr_time_now() + apr_time_from_sec(1); + *pnext_run = ctx.time; + + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "ocsp_renew done"); + } + return; +} + +apr_status_t md_ocsp_remove_responses_older_than(md_ocsp_reg_t *reg, apr_pool_t *p, + apr_time_t timestamp) +{ + return md_store_remove_not_modified_since(reg->store, p, timestamp, + MD_SG_OCSP, "*", "ocsp*.json"); +} + +typedef struct { + apr_pool_t *p; + md_ocsp_reg_t *reg; + int good; + int revoked; + int unknown; +} ocsp_summary_ctx_t; + +static int add_to_summary(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + ocsp_summary_ctx_t *ctx = baton; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + md_ocsp_cert_stat_t stat; + md_timeperiod_t valid; + + (void)key; + (void)klen; + ocsp_get_meta(&stat, &valid, ctx->reg, ostat, ctx->p); + switch (stat) { + case MD_OCSP_CERT_ST_GOOD: ++ctx->good; break; + case MD_OCSP_CERT_ST_REVOKED: ++ctx->revoked; break; + case MD_OCSP_CERT_ST_UNKNOWN: ++ctx->unknown; break; + } + return 1; +} + +void md_ocsp_get_summary(md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p) +{ + md_json_t *json; + ocsp_summary_ctx_t ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.p = p; + ctx.reg = reg; + apr_hash_do(add_to_summary, &ctx, reg->hash); + + json = md_json_create(p); + md_json_setl(ctx.good+ctx.revoked+ctx.unknown, json, MD_KEY_TOTAL, NULL); + md_json_setl(ctx.good, json, MD_KEY_GOOD, NULL); + md_json_setl(ctx.revoked, json, MD_KEY_REVOKED, NULL); + md_json_setl(ctx.unknown, json, MD_KEY_UNKNOWN, NULL); + *pjson = json; +} + +static apr_status_t job_loadj(md_json_t **pjson, const char *name, + md_ocsp_reg_t *reg, apr_pool_t *p) +{ + return md_store_load_json(reg->store, MD_SG_OCSP, name, MD_FN_JOB, pjson, p); +} + +typedef struct { + apr_pool_t *p; + md_ocsp_reg_t *reg; + apr_array_header_t *ostats; +} ocsp_status_ctx_t; + +static md_json_t *mk_jstat(md_ocsp_status_t *ostat, md_ocsp_reg_t *reg, apr_pool_t *p) +{ + md_ocsp_cert_stat_t stat; + md_timeperiod_t valid, renewal; + md_json_t *json, *jobj; + apr_status_t rv; + + json = md_json_create(p); + md_json_sets(ostat->md_name, json, MD_KEY_DOMAIN, NULL); + md_json_sets(ostat->hexid, json, MD_KEY_ID, NULL); + ocsp_get_meta(&stat, &valid, reg, ostat, p); + md_json_sets(md_ocsp_cert_stat_name(stat), json, MD_KEY_STATUS, NULL); + md_json_sets(ostat->hex_sha256, json, MD_KEY_CERT, MD_KEY_SHA256_FINGERPRINT, NULL); + md_json_sets(ostat->responder_url, json, MD_KEY_URL, NULL); + md_json_set_timeperiod(&valid, json, MD_KEY_VALID, NULL); + renewal = md_timeperiod_slice_before_end(&valid, ®->renew_window); + md_json_set_time(renewal.start, json, MD_KEY_RENEW_AT, NULL); + if ((MD_OCSP_CERT_ST_UNKNOWN == stat) || renewal.start < apr_time_now()) { + /* We have no answer yet, or it should be in renew now. Add job information */ + rv = job_loadj(&jobj, ostat->md_name, reg, p); + if (APR_SUCCESS == rv) { + md_json_setj(jobj, json, MD_KEY_RENEWAL, NULL); + } + } + return json; +} + +static int add_ostat(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + ocsp_status_ctx_t *ctx = baton; + const md_ocsp_status_t *ostat = val; + + (void)key; + (void)klen; + APR_ARRAY_PUSH(ctx->ostats, const md_ocsp_status_t*) = ostat; + return 1; +} + +static int md_ostat_cmp(const void *v1, const void *v2) +{ + int n; + n = strcmp((*(md_ocsp_status_t**)v1)->md_name, (*(md_ocsp_status_t**)v2)->md_name); + if (!n) { + n = strcmp((*(md_ocsp_status_t**)v1)->hexid, (*(md_ocsp_status_t**)v2)->hexid); + } + return n; +} + +void md_ocsp_get_status_all(md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p) +{ + md_json_t *json; + ocsp_status_ctx_t ctx; + md_ocsp_status_t *ostat; + int i; + + memset(&ctx, 0, sizeof(ctx)); + ctx.p = p; + ctx.reg = reg; + ctx.ostats = apr_array_make(p, (int)apr_hash_count(reg->hash), sizeof(md_ocsp_status_t*)); + json = md_json_create(p); + + apr_hash_do(add_ostat, &ctx, reg->hash); + qsort(ctx.ostats->elts, (size_t)ctx.ostats->nelts, sizeof(md_json_t*), md_ostat_cmp); + + for (i = 0; i < ctx.ostats->nelts; ++i) { + ostat = APR_ARRAY_IDX(ctx.ostats, i, md_ocsp_status_t*); + md_json_addj(mk_jstat(ostat, reg, p), json, MD_KEY_OCSPS, NULL); + } + *pjson = json; +} + +void md_ocsp_set_notify_cb(md_ocsp_reg_t *ocsp, md_job_notify_cb *cb, void *baton) +{ + ocsp->notify = cb; + ocsp->notify_ctx = baton; +} + +md_job_t *md_ocsp_job_make(md_ocsp_reg_t *ocsp, const char *mdomain, apr_pool_t *p) +{ + md_job_t *job; + + job = md_job_make(p, ocsp->store, MD_SG_OCSP, mdomain); + md_job_set_notify_cb(job, ocsp->notify, ocsp->notify_ctx); + return job; +} diff --git a/modules/md/md_ocsp.h b/modules/md/md_ocsp.h new file mode 100644 index 0000000000..9f0c0fd035 --- /dev/null +++ b/modules/md/md_ocsp.h @@ -0,0 +1,66 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef md_ocsp_h +#define md_ocsp_h + +struct md_job_t; +struct md_json_t; +struct md_result_t; +struct md_store_t; +struct md_timeslice_t; + +typedef enum { + MD_OCSP_CERT_ST_UNKNOWN, + MD_OCSP_CERT_ST_GOOD, + MD_OCSP_CERT_ST_REVOKED, +} md_ocsp_cert_stat_t; + +const char *md_ocsp_cert_stat_name(md_ocsp_cert_stat_t stat); +md_ocsp_cert_stat_t md_ocsp_cert_stat_value(const char *name); + +typedef struct md_ocsp_reg_t md_ocsp_reg_t; + +apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, + struct md_store_t *store, + const md_timeslice_t *renew_window, + const char *user_agent, const char *proxy_url); + +apr_status_t md_ocsp_prime(md_ocsp_reg_t *reg, md_cert_t *x, + md_cert_t *issuer, const md_t *md); + +apr_status_t md_ocsp_get_status(unsigned char **pder, int *pderlen, + md_ocsp_reg_t *reg, const md_cert_t *cert, + apr_pool_t *p, const md_t *md); + +apr_status_t md_ocsp_get_meta(md_ocsp_cert_stat_t *pstat, md_timeperiod_t *pvalid, + md_ocsp_reg_t *reg, const md_cert_t *cert, + apr_pool_t *p, const md_t *md); + +apr_size_t md_ocsp_count(md_ocsp_reg_t *reg); + +void md_ocsp_renew(md_ocsp_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, apr_time_t *pnext_run); + +apr_status_t md_ocsp_remove_responses_older_than(md_ocsp_reg_t *reg, apr_pool_t *p, + apr_time_t timestamp); + +void md_ocsp_get_summary(struct md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p); +void md_ocsp_get_status_all(struct md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p); + +void md_ocsp_set_notify_cb(md_ocsp_reg_t *reg, md_job_notify_cb *cb, void *baton); +struct md_job_t *md_ocsp_job_make(md_ocsp_reg_t *ocsp, const char *mdomain, apr_pool_t *p); + +#endif /* md_ocsp_h */ diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c index 0c4c9244a8..4092edeb6e 100644 --- a/modules/md/md_reg.c +++ b/modules/md/md_reg.c @@ -46,8 +46,10 @@ struct md_reg_t { int can_https; const char *proxy_url; int domains_frozen; - const md_timeslice_t *renew_window; - const md_timeslice_t *warn_window; + md_timeslice_t *renew_window; + md_timeslice_t *warn_window; + md_job_notify_cb *notify; + void *notify_ctx; }; /**************************************************************************************************/ @@ -292,11 +294,6 @@ md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p) return NULL; } -apr_status_t md_reg_reinit_state(md_reg_t *reg, md_t *md, apr_pool_t *p) -{ - return state_init(reg, p, md); -} - typedef struct { const char *domain; md_t *md; @@ -417,7 +414,7 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v return APR_ENOENT; } - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "update md %s", name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "md[%s]: update store", name); if (do_checks && APR_SUCCESS != (rv = check_values(reg, ptemp, updates, fields))) { return rv; @@ -455,11 +452,11 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v } if (MD_UPD_RENEW_WINDOW & fields) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update renew-window: %s", name); - nmd->renew_window = updates->renew_window; + *nmd->renew_window = *updates->renew_window; } if (MD_UPD_WARN_WINDOW & fields) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update warn-window: %s", name); - nmd->warn_window = updates->warn_window; + *nmd->warn_window = *updates->warn_window; } if (MD_UPD_CA_CHALLENGES & fields) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca challenges: %s", name); @@ -489,6 +486,10 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update proto: %s", name); nmd->acme_tls_1_domains = updates->acme_tls_1_domains; } + if (MD_UPD_STAPLING & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update stapling: %s", name); + nmd->stapling = updates->stapling; + } if (fields && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, nmd, 0))) { rv = state_init(reg, ptemp, nmd); @@ -496,17 +497,11 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v return rv; } -static apr_status_t update_md(md_reg_t *reg, apr_pool_t *p, - const char *name, const md_t *md, - int fields, int do_checks) -{ - return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, do_checks, NULL); -} - apr_status_t md_reg_update(md_reg_t *reg, apr_pool_t *p, - const char *name, const md_t *md, int fields) + const char *name, const md_t *md, int fields, + int do_checks) { - return update_md(reg, p, name, md, fields, 1); + return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, do_checks, NULL); } apr_status_t md_reg_delete_acct(md_reg_t *reg, apr_pool_t *p, const char *acct_id) @@ -612,16 +607,16 @@ apr_status_t md_reg_get_cred_files(const char **pkeyfile, const char **pcertfile return APR_SUCCESS; } -int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p) +apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p) { const md_pubcert_t *pub; const md_cert_t *cert; md_timeperiod_t certlife, renewal; apr_status_t rv; - if (md->state == MD_S_INCOMPLETE) return 1; + if (md->state == MD_S_INCOMPLETE) return apr_time_now(); rv = md_reg_get_pubcert(&pub, reg, md, p); - if (APR_STATUS_IS_ENOENT(rv)) return 1; + if (APR_STATUS_IS_ENOENT(rv)) return apr_time_now(); if (APR_SUCCESS == rv) { cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*); certlife.start = md_cert_get_not_before(cert); @@ -629,16 +624,24 @@ int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p) renewal = md_timeperiod_slice_before_end(&certlife, md->renew_window); if (md_log_is_level(p, MD_LOG_TRACE1)) { - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, "md[%s]: cert-life[%s] renewal[%s]", md->name, md_timeperiod_print(p, &certlife), md_timeperiod_print(p, &renewal)); } - return md_timeperiod_has_started(&renewal, apr_time_now()); + return renewal.start; } return 0; } +int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p) +{ + apr_time_t renew_at; + + renew_at = md_reg_renew_at(reg, md, p); + return renew_at && (renew_at <= apr_time_now()); +} + int md_reg_should_warn(md_reg_t *reg, const md_t *md, apr_pool_t *p) { const md_pubcert_t *pub; @@ -656,7 +659,7 @@ int md_reg_should_warn(md_reg_t *reg, const md_t *md, apr_pool_t *p) warn = md_timeperiod_slice_before_end(&certlife, md->warn_window); if (md_log_is_level(p, MD_LOG_TRACE1)) { - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, "md[%s]: cert-life[%s] warn[%s]", md->name, md_timeperiod_print(p, &certlife), md_timeperiod_print(p, &warn)); @@ -669,33 +672,6 @@ int md_reg_should_warn(md_reg_t *reg, const md_t *md, apr_pool_t *p) /**************************************************************************************************/ /* synching */ -typedef struct { - apr_pool_t *p; - apr_array_header_t *store_mds; -} sync_ctx; - -static int do_add_md(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp) -{ - sync_ctx *ctx = baton; - - (void)store; - (void)ptemp; - APR_ARRAY_PUSH(ctx->store_mds, const md_t*) = md_clone(ctx->p, md); - return 1; -} - -static apr_status_t read_store_mds(md_reg_t *reg, sync_ctx *ctx) -{ - int rv; - - apr_array_clear(ctx->store_mds); - rv = md_store_md_iter(do_add_md, ctx, reg->store, ctx->p, MD_SG_DOMAINS, "*"); - if (APR_STATUS_IS_ENOENT(rv) || APR_STATUS_IS_EINVAL(rv)) { - rv = APR_SUCCESS; - } - return rv; -} - apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https) { if (reg->can_http != can_http || reg->can_https != can_https) { @@ -714,192 +690,211 @@ apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int ca return APR_SUCCESS; } -static apr_status_t update_md(md_reg_t *reg, apr_pool_t *p, - const char *name, const md_t *md, - int fields, int do_checks); - -/** - * Procedure: - * 1. Collect all defined "managed domains" (MD). It does not matter where a MD is defined. - * All MDs need to be unique and have no overlaps in their domain names. - * Fail the config otherwise. Also, if a vhost matches an MD, it - * needs to *only* have ServerAliases from that MD. There can be no more than one - * matching MD for a vhost. But an MD can apply to several vhosts. - * 2. Synchronize with the persistent store. Iterate over all configured MDs and - * a. create them in the store if they do not already exist, neither under the - * name or with a common domain. - * b. compare domain lists from store and config, if - * - store has dns name in other MD than from config, remove dns name from store def, - * issue WARNING. - * - store misses dns name from config, add dns name and update store - * c. compare MD acme url/protocol, update if changed +static md_t *find_closest_match(apr_array_header_t *mds, const md_t *md) +{ + md_t *candidate, *m; + apr_size_t cand_n, n; + int i; + + candidate = md_get_by_name(mds, md->name); + if (!candidate) { + /* try to find an instance that contains all domain names from md */ + for (i = 0; i < mds->nelts; ++i) { + m = APR_ARRAY_IDX(mds, i, md_t *); + if (md_contains_domains(m, md)) { + return m; + } + } + /* no matching name and no md in the list has all domains. + * We consider that managed domain as closest match that contains at least one + * domain name from md, ONLY if there is no other one that also has. + */ + cand_n = 0; + for (i = 0; i < mds->nelts; ++i) { + m = APR_ARRAY_IDX(mds, i, md_t *); + n = md_common_name_count(md, m); + if (n > cand_n) { + candidate = m; + cand_n = n; + } + } + } + return candidate; +} + +typedef struct { + apr_pool_t *p; + apr_array_header_t *master_mds; + apr_array_header_t *store_names; + apr_array_header_t *maybe_new_mds; + apr_array_header_t *new_mds; + apr_array_header_t *unassigned_mds; +} sync_ctx_v2; + +static int iter_add_name(void *baton, const char *dir, const char *name, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp) +{ + sync_ctx_v2 *ctx = baton; + + (void)dir; + (void)value; + (void)ptemp; + (void)vtype; + APR_ARRAY_PUSH(ctx->store_names, const char*) = apr_pstrdup(ctx->p, name); + return APR_SUCCESS; +} + +/* A better scaling version: + * 1. The consistency of the MDs in 'master_mds' has already been verified. E.g. + * that no domain lists overlap etc. + * 2. All MD storage that exists will be overwritten by the settings we have. + * And "exists" meaning that "store/MD_SG_DOMAINS/name" exists. + * 3. For MDs that have no directory in "store/MD_SG_DOMAINS", we load all MDs + * outside the list of known names from MD_SG_DOMAINS. In this list, we + * look for the MD with the most domain overlap. + * - if we find it, we assume this is a rename and move the old MD to the new name. + * - if not, MD is completely new. + * 4. Any MD in store that does not match the "master_mds" will just be left as is. */ -apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, - apr_array_header_t *master_mds) +apr_status_t md_reg_sync_start(md_reg_t *reg, apr_array_header_t *master_mds, apr_pool_t *p) { - sync_ctx ctx; + sync_ctx_v2 ctx; apr_status_t rv; - - ctx.p = ptemp; - ctx.store_mds = apr_array_make(ptemp, 100, sizeof(md_t *)); - rv = read_store_mds(reg, &ctx); + md_t *md, *oldmd; + const char *name; + int i, idx; - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, - "sync: found %d mds in store", ctx.store_mds->nelts); - if (reg->domains_frozen) return APR_EACCES; - if (APR_SUCCESS == rv) { - int i, fields; - md_t *md, *config_md, *smd, *omd; - const char *common; - - for (i = 0; i < master_mds->nelts; ++i) { - md = APR_ARRAY_IDX(master_mds, i, md_t *); - - /* find the store md that is closest match for the configured md */ - smd = md_find_closest_match(ctx.store_mds, md); - if (smd) { - fields = 0; - - /* Did the name change? This happens when the order of names in configuration - * changes or when the first name is removed. Use the name from the store, but - * remember the original one. We try to align this later on. */ - if (strcmp(md->name, smd->name)) { - md->configured_name = md->name; - md->name = apr_pstrdup(p, smd->name); - } - - /* Make the stored domain list *exactly* the same, even if - * someone only changed upper/lowercase, we'd like to persist that. */ - if (!md_equal_domains(md, smd, 1)) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, - "%s: domains changed", smd->name); - smd->domains = md_array_str_clone(ptemp, md->domains); - fields |= MD_UPD_DOMAINS; - } - - /* Look for other store mds which have domains now being part of smd */ - while (APR_SUCCESS == rv && (omd = md_get_by_dns_overlap(ctx.store_mds, md))) { - /* find the name now duplicate */ - common = md_common_name(md, omd); - assert(common); - - /* Is this md still configured or has it been abandoned in the config? */ - config_md = md_get_by_name(master_mds, omd->name); - if (config_md && md_contains(config_md, common, 0)) { - /* domain used in two configured mds, not allowed */ - rv = APR_EINVAL; - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, - "domain %s used in md %s and %s", - common, md->name, omd->name); - } - else { - /* remove it from the other md and update store, or, if it - * is now empty, move it into the archive */ - omd->domains = md_array_str_remove(ptemp, omd->domains, common, 0); - if (apr_is_empty_array(omd->domains)) { - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, - "All domains of the MD %s have moved elsewhere, " - " moving it to the archive. ", omd->name); - md_reg_remove(reg, ptemp, omd->name, 1); /* best effort */ - } - else { - rv = update_md(reg, ptemp, omd->name, omd, MD_UPD_DOMAINS, 0); - } - } - } - - /* If no CA url/proto is configured for the MD, take the default */ - if (!md->ca_url) { - md->ca_url = MD_ACME_DEF_URL; - md->ca_proto = MD_PROTO_ACME; - } - - if (MD_SVAL_UPDATE(md, smd, ca_url)) { - smd->ca_url = md->ca_url; - fields |= MD_UPD_CA_URL; - } - if (MD_SVAL_UPDATE(md, smd, ca_proto)) { - smd->ca_proto = md->ca_proto; - fields |= MD_UPD_CA_PROTO; - } - if (MD_SVAL_UPDATE(md, smd, ca_agreement)) { - smd->ca_agreement = md->ca_agreement; - fields |= MD_UPD_AGREEMENT; - } - if (MD_VAL_UPDATE(md, smd, transitive)) { - smd->transitive = md->transitive; - fields |= MD_UPD_TRANSITIVE; - } - if (MD_VAL_UPDATE(md, smd, renew_mode)) { - smd->renew_mode = md->renew_mode; - fields |= MD_UPD_DRIVE_MODE; - } - if (!apr_is_empty_array(md->contacts) - && !md_array_str_eq(md->contacts, smd->contacts, 0)) { - smd->contacts = md->contacts; - fields |= MD_UPD_CONTACTS; - } - if (!md_timeslice_eq(md->renew_window, smd->renew_window)) { - smd->renew_window = md->renew_window; - fields |= MD_UPD_RENEW_WINDOW; - } - if (!md_timeslice_eq(md->warn_window, smd->warn_window)) { - smd->warn_window = md->warn_window; - fields |= MD_UPD_WARN_WINDOW; - } - if (md->ca_challenges) { - md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0); - if (!smd->ca_challenges - || !md_array_str_eq(md->ca_challenges, smd->ca_challenges, 0)) { - smd->ca_challenges = apr_array_copy(ptemp, md->ca_challenges); - fields |= MD_UPD_CA_CHALLENGES; - } - } - else if (smd->ca_challenges) { - smd->ca_challenges = NULL; - fields |= MD_UPD_CA_CHALLENGES; - } - if (!md_pkey_spec_eq(md->pkey_spec, smd->pkey_spec)) { - fields |= MD_UPD_PKEY_SPEC; - smd->pkey_spec = NULL; - if (md->pkey_spec) { - smd->pkey_spec = apr_pmemdup(p, md->pkey_spec, sizeof(md_pkey_spec_t)); - } - } - if (MD_VAL_UPDATE(md, smd, require_https)) { - smd->require_https = md->require_https; - fields |= MD_UPD_REQUIRE_HTTPS; - } - if (MD_VAL_UPDATE(md, smd, must_staple)) { - smd->must_staple = md->must_staple; - fields |= MD_UPD_MUST_STAPLE; - } - if (!md_array_str_eq(md->acme_tls_1_domains, smd->acme_tls_1_domains, 0)) { - smd->acme_tls_1_domains = md->acme_tls_1_domains; - fields |= MD_UPD_PROTO; - } - - if (fields) { - rv = update_md(reg, ptemp, smd->name, smd, fields, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md %s updated", smd->name); - } - } - else { - /* new managed domain */ - /* If no CA url/proto is configured for the MD, take the default */ - if (!md->ca_url) { - md->ca_url = MD_ACME_DEF_URL; - md->ca_proto = MD_PROTO_ACME; - } - rv = add_md(reg, md, ptemp, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "new md %s added", md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "sync MDs, start"); + + ctx.p = p; + ctx.master_mds = master_mds; + ctx.store_names = apr_array_make(p, master_mds->nelts + 100, sizeof(const char*)); + ctx.maybe_new_mds = apr_array_make(p, master_mds->nelts, sizeof(md_t*)); + ctx.new_mds = apr_array_make(p, master_mds->nelts, sizeof(md_t*)); + ctx.unassigned_mds = apr_array_make(p, master_mds->nelts, sizeof(md_t*)); + + rv = md_store_iter_names(iter_add_name, &ctx, reg->store, p, MD_SG_DOMAINS, "*"); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "listing existing store MD names"); + goto leave; + } + + /* Get all MDs that are not already present in store */ + for (i = 0; i < ctx.master_mds->nelts; ++i) { + md = APR_ARRAY_IDX(ctx.master_mds, i, md_t*); + idx = md_array_str_index(ctx.store_names, md->name, 0, 1); + if (idx < 0) { + APR_ARRAY_PUSH(ctx.maybe_new_mds, md_t*) = md; + md_array_remove_at(ctx.store_names, idx); + } + } + + if (ctx.maybe_new_mds->nelts == 0) goto leave; /* none new */ + if (ctx.store_names->nelts == 0) goto leave; /* all new */ + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, %d potentially new MDs detected, looking for renames among " + "the %d unassigned store domains", (int)ctx.maybe_new_mds->nelts, + (int)ctx.store_names->nelts); + for (i = 0; i < ctx.store_names->nelts; ++i) { + name = APR_ARRAY_IDX(ctx.store_names, i, const char*); + if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, name, &md, p)) { + APR_ARRAY_PUSH(ctx.unassigned_mds, md_t*) = md; + } + } + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, %d MDs maybe new, checking store", (int)ctx.maybe_new_mds->nelts); + for (i = 0; i < ctx.maybe_new_mds->nelts; ++i) { + md = APR_ARRAY_IDX(ctx.maybe_new_mds, i, md_t*); + oldmd = find_closest_match(ctx.unassigned_mds, md); + if (oldmd) { + /* found the rename, move the domains and possible staging directory */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, found MD %s under previous name %s", md->name, oldmd->name); + rv = md_store_rename(reg->store, p, MD_SG_DOMAINS, oldmd->name, md->name); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "sync MDs, renaming MD %s to %s failed", oldmd->name, md->name); + /* ignore it? */ } + md_store_rename(reg->store, p, MD_SG_STAGING, oldmd->name, md->name); + md_array_remove(ctx.unassigned_mds, oldmd); + } + else { + APR_ARRAY_PUSH(ctx.new_mds, md_t*) = md; } } - else { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading mds"); + +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, %d existing, %d moved, %d new.", + (int)ctx.master_mds->nelts - ctx.maybe_new_mds->nelts, + (int)ctx.maybe_new_mds->nelts - ctx.new_mds->nelts, + (int)ctx.new_mds->nelts); + return rv; +} + +/** + * Finish synching an MD with the store. + * 1. if there are changed properties (or if the MD is new), save it. + * 2. read any existing certificate and init the state of the memory MD + */ +apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool_t *ptemp) +{ + md_t *old; + apr_status_t rv; + int changed = 1; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "sync MDs, finish start"); + + if (!md->ca_url) { + md->ca_url = MD_ACME_DEF_URL; + md->ca_proto = MD_PROTO_ACME; } + rv = state_init(reg, ptemp, md); + if (APR_SUCCESS != rv) goto leave; + + if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, md->name, &old, ptemp)) { + /* Some parts are kept from old, lacking new values */ + if ((!md->contacts || apr_is_empty_array(md->contacts)) && old->contacts) { + md->contacts = md_array_str_clone(p, old->contacts); + } + if (md->ca_challenges && old->ca_challenges) { + if (!md_array_str_eq(md->ca_challenges, old->ca_challenges, 0)) { + md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0); + } + } + if (!md->ca_account && old->ca_account) { + md->ca_account = apr_pstrdup(p, old->ca_account); + } + + /* if everything remains the same, spare the write back */ + if (!MD_VAL_UPDATE(md, old, state) + && !MD_SVAL_UPDATE(md, old, ca_url) + && !MD_SVAL_UPDATE(md, old, ca_proto) + && !MD_SVAL_UPDATE(md, old, ca_agreement) + && !MD_VAL_UPDATE(md, old, transitive) + && md_equal_domains(md, old, 1) + && !MD_VAL_UPDATE(md, old, renew_mode) + && md_timeslice_eq(md->renew_window, old->renew_window) + && md_timeslice_eq(md->warn_window, old->warn_window) + && md_pkey_spec_eq(md->pkey_spec, old->pkey_spec) + && !MD_VAL_UPDATE(md, old, require_https) + && !MD_VAL_UPDATE(md, old, must_staple) + && md_array_str_eq(md->acme_tls_1_domains, old->acme_tls_1_domains, 0) + && !MD_VAL_UPDATE(md, old, stapling) + && md_array_str_eq(md->contacts, old->contacts, 0) + && md_array_str_eq(md->ca_challenges, old->ca_challenges, 0)) { + changed = 0; + } + } + if (changed) { + rv = md_save(reg->store, ptemp, MD_SG_DOMAINS, md, 0); + } +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "sync MDs, finish done"); return rv; } @@ -970,11 +965,14 @@ static apr_status_t run_init(void *baton, apr_pool_t *p, ...) md_proto_driver_t *driver, **pdriver; md_result_t *result; apr_table_t *env; + const char *s; + int preload; (void)p; va_start(ap, p); pdriver = va_arg(ap, md_proto_driver_t **); md = va_arg(ap, const md_t *); + preload = va_arg(ap, int); env = va_arg(ap, apr_table_t *); result = va_arg(ap, md_result_t *); va_end(ap); @@ -989,6 +987,11 @@ static apr_status_t run_init(void *baton, apr_pool_t *p, ...) driver->md = md; driver->can_http = reg->can_http; driver->can_https = reg->can_https; + + s = apr_table_get(driver->env, MD_KEY_ACTIVATION_DELAY); + if (!s || APR_SUCCESS != md_duration_parse(&driver->activation_delay, s, "d")) { + driver->activation_delay = apr_time_from_sec(MD_SECS_PER_DAY); + } if (!md->ca_proto) { md_result_printf(result, APR_EGENERAL, "CA protocol is not defined"); @@ -1002,7 +1005,12 @@ static apr_status_t run_init(void *baton, apr_pool_t *p, ...) goto leave; } - result->status = driver->proto->init(driver, result); + if (preload) { + result->status = driver->proto->init_preload(driver, result); + } + else { + result->status = driver->proto->init(driver, result); + } leave: if (APR_SUCCESS != result->status) { @@ -1027,7 +1035,7 @@ static apr_status_t run_test_init(void *baton, apr_pool_t *p, apr_pool_t *ptemp, env = va_arg(ap, apr_table_t *); result = va_arg(ap, md_result_t *); - return run_init(baton, ptemp, &driver, md, env, result, NULL); + return run_init(baton, ptemp, &driver, md, 0, env, result, NULL); } apr_status_t md_reg_test_init(md_reg_t *reg, const md_t *md, struct apr_table_t *env, @@ -1051,7 +1059,7 @@ static apr_status_t run_renew(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_ reset = va_arg(ap, int); result = va_arg(ap, md_result_t *); - rv = run_init(baton, ptemp, &driver, md, env, result, NULL); + rv = run_init(baton, ptemp, &driver, md, 0, env, result, NULL); if (APR_SUCCESS == rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run staging", md->name); driver->reset = reset; @@ -1089,11 +1097,11 @@ static apr_status_t run_load_staging(void *baton, apr_pool_t *p, apr_pool_t *pte result = va_arg(ap, md_result_t*); if (APR_STATUS_IS_ENOENT(rv = md_load(reg->store, MD_SG_STAGING, md->name, NULL, ptemp))) { - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, ptemp, "%s: nothing staged", md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, ptemp, "%s: nothing staged", md->name); goto out; } - rv = run_init(baton, ptemp, &driver, md, env, result, NULL); + rv = run_init(baton, ptemp, &driver, md, 1, env, result, NULL); if (APR_SUCCESS != rv) goto out; apr_hash_set(reg->certs, md->name, (apr_ssize_t)strlen(md->name), NULL); @@ -1102,9 +1110,10 @@ static apr_status_t run_load_staging(void *baton, apr_pool_t *p, apr_pool_t *pte if (APR_SUCCESS != rv) goto out; /* If we had a job saved in STAGING, copy it over too */ - job = md_job_make(ptemp, md->name); - if (APR_SUCCESS == md_job_load(job, reg, MD_SG_STAGING, ptemp)) { - md_job_save(job, reg, MD_SG_TMP, NULL, ptemp); + job = md_reg_job_make(reg, md->name, ptemp); + if (APR_SUCCESS == md_job_load(job)) { + md_job_set_group(job, MD_SG_TMP); + md_job_save(job, NULL, ptemp); } /* swap */ @@ -1118,9 +1127,13 @@ static apr_status_t run_load_staging(void *baton, apr_pool_t *p, apr_pool_t *pte md_store_purge(reg->store, p, MD_SG_STAGING, md->name); md_store_purge(reg->store, p, MD_SG_CHALLENGES, md->name); md_result_set(result, APR_SUCCESS, "new certificate successfully saved in domains"); - + md_job_notify(job, "installed", result); + if (job->dirty) md_job_save(job, result, ptemp); + out: - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: load done", md->name); + if (!APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "%s: load done", md->name); + } return rv; } @@ -1150,12 +1163,27 @@ leave: return rv; } -void md_reg_set_renew_window_default(md_reg_t *reg, const md_timeslice_t *renew_window) +void md_reg_set_renew_window_default(md_reg_t *reg, md_timeslice_t *renew_window) +{ + *reg->renew_window = *renew_window; +} + +void md_reg_set_warn_window_default(md_reg_t *reg, md_timeslice_t *warn_window) { - reg->renew_window = renew_window; + *reg->warn_window = *warn_window; } -void md_reg_set_warn_window_default(md_reg_t *reg, const md_timeslice_t *warn_window) +void md_reg_set_notify_cb(md_reg_t *reg, md_job_notify_cb *cb, void *baton) { - reg->warn_window = warn_window; + reg->notify = cb; + reg->notify_ctx = baton; +} + +md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p) +{ + md_job_t *job; + + job = md_job_make(p, reg->store, MD_SG_STAGING, mdomain); + md_job_set_notify_cb(job, reg->notify, reg->notify_ctx); + return job; } diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h index c026af1db1..adf2b1ef46 100644 --- a/modules/md/md_reg.h +++ b/modules/md/md_reg.h @@ -19,11 +19,12 @@ struct apr_hash_t; struct apr_array_header_t; -struct md_store_t; struct md_pkey_t; struct md_cert_t; struct md_result_t; +#include "md_store.h" + /** * A registry for managed domains with a md_store_t as persistence. * @@ -33,10 +34,10 @@ typedef struct md_reg_t md_reg_t; /** * Create the MD registry, using the pool and store. */ -apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *pm, struct md_store_t *store, +apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *pm, md_store_t *store, const char *proxy_url); -struct md_store_t *md_reg_store_get(md_reg_t *reg); +md_store_t *md_reg_store_get(md_reg_t *reg); apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https); @@ -66,11 +67,6 @@ md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, a md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p); /** - * Re-compute the state of the MD, given current store contents. - */ -apr_status_t md_reg_reinit_state(md_reg_t *reg, md_t *md, apr_pool_t *p); - -/** * Callback invoked for every md in the registry. If 0 is returned, iteration stops. */ typedef int md_reg_do_cb(void *baton, md_reg_t *reg, md_t *md); @@ -85,21 +81,22 @@ int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p); /** * Bitmask for fields that are updated. */ -#define MD_UPD_DOMAINS 0x0001 -#define MD_UPD_CA_URL 0x0002 -#define MD_UPD_CA_PROTO 0x0004 -#define MD_UPD_CA_ACCOUNT 0x0008 -#define MD_UPD_CONTACTS 0x0010 -#define MD_UPD_AGREEMENT 0x0020 -#define MD_UPD_DRIVE_MODE 0x0080 -#define MD_UPD_RENEW_WINDOW 0x0100 -#define MD_UPD_CA_CHALLENGES 0x0200 -#define MD_UPD_PKEY_SPEC 0x0400 -#define MD_UPD_REQUIRE_HTTPS 0x0800 -#define MD_UPD_TRANSITIVE 0x1000 -#define MD_UPD_MUST_STAPLE 0x2000 -#define MD_UPD_PROTO 0x4000 -#define MD_UPD_WARN_WINDOW 0x8000 +#define MD_UPD_DOMAINS 0x00001 +#define MD_UPD_CA_URL 0x00002 +#define MD_UPD_CA_PROTO 0x00004 +#define MD_UPD_CA_ACCOUNT 0x00008 +#define MD_UPD_CONTACTS 0x00010 +#define MD_UPD_AGREEMENT 0x00020 +#define MD_UPD_DRIVE_MODE 0x00080 +#define MD_UPD_RENEW_WINDOW 0x00100 +#define MD_UPD_CA_CHALLENGES 0x00200 +#define MD_UPD_PKEY_SPEC 0x00400 +#define MD_UPD_REQUIRE_HTTPS 0x00800 +#define MD_UPD_TRANSITIVE 0x01000 +#define MD_UPD_MUST_STAPLE 0x02000 +#define MD_UPD_PROTO 0x04000 +#define MD_UPD_WARN_WINDOW 0x08000 +#define MD_UPD_STAPLING 0x10000 #define MD_UPD_ALL 0x7FFFFFFF /** @@ -107,7 +104,8 @@ int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p); * values from the given md, all other values remain unchanged. */ apr_status_t md_reg_update(md_reg_t *reg, apr_pool_t *p, - const char *name, const md_t *md, int fields); + const char *name, const md_t *md, + int fields, int check_consistency); /** * Get the chain of public certificates of the managed domain md, starting with the cert @@ -127,8 +125,13 @@ apr_status_t md_reg_get_cred_files(const char **pkeyfile, const char **pcertfile /** * Synchronise the give master mds with the store. */ -apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, - apr_array_header_t *master_mds); +apr_status_t md_reg_sync_start(md_reg_t *reg, apr_array_header_t *master_mds, apr_pool_t *p); + +/** + * Re-compute the state of the MD, given current store contents. + */ +apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool_t *ptemp); + apr_status_t md_reg_remove(md_reg_t *reg, apr_pool_t *p, const char *name, int archive); @@ -164,6 +167,12 @@ apr_status_t md_reg_freeze_domains(md_reg_t *reg, apr_array_header_t *mds); int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p); /** + * Return the timestamp when the certificate should be renewed. A value of 0 + * indicates that that renewal is not configured (see renew_mode). + */ +apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p); + +/** * Return if a warning should be issued about the certificate expiration. * This applies the configured warn window to the remaining lifetime of the * current certiciate. If no certificate is present, this returns 0. @@ -188,17 +197,19 @@ struct md_proto_driver_t { struct apr_table_t *env; md_reg_t *reg; - struct md_store_t *store; + md_store_t *store; const char *proxy_url; const md_t *md; int can_http; int can_https; int reset; + apr_interval_time_t activation_delay; }; typedef apr_status_t md_proto_init_cb(md_proto_driver_t *driver, struct md_result_t *result); typedef apr_status_t md_proto_renew_cb(md_proto_driver_t *driver, struct md_result_t *result); +typedef apr_status_t md_proto_init_preload_cb(md_proto_driver_t *driver, struct md_result_t *result); typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver, md_store_group_t group, struct md_result_t *result); @@ -206,6 +217,7 @@ struct md_proto_t { const char *protocol; md_proto_init_cb *init; md_proto_renew_cb *renew; + md_proto_init_preload_cb *init_preload; md_proto_preload_cb *preload; }; @@ -239,7 +251,10 @@ apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, apr_status_t md_reg_load_staging(md_reg_t *reg, const md_t *md, struct apr_table_t *env, struct md_result_t *result, apr_pool_t *p); -void md_reg_set_renew_window_default(md_reg_t *reg, const md_timeslice_t *renew_window); -void md_reg_set_warn_window_default(md_reg_t *reg, const md_timeslice_t *warn_window); +void md_reg_set_renew_window_default(md_reg_t *reg, md_timeslice_t *renew_window); +void md_reg_set_warn_window_default(md_reg_t *reg, md_timeslice_t *warn_window); + +void md_reg_set_notify_cb(md_reg_t *reg, md_job_notify_cb *cb, void *baton); +struct md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p); #endif /* mod_md_md_reg_h */ diff --git a/modules/md/md_result.c b/modules/md/md_result.c index 4076d5b565..7d8370fadd 100644 --- a/modules/md/md_result.c +++ b/modules/md/md_result.c @@ -42,14 +42,15 @@ md_result_t *md_result_make(apr_pool_t *p, apr_status_t status) result = apr_pcalloc(p, sizeof(*result)); result->p = p; + result->md_name = MD_OTHER; result->status = status; return result; } -md_result_t *md_result_md_make(apr_pool_t *p, const struct md_t *md) +md_result_t *md_result_md_make(apr_pool_t *p, const char *md_name) { md_result_t *result = md_result_make(p, APR_SUCCESS); - result->md = md; + result->md_name = md_name; return result; } @@ -74,6 +75,7 @@ void md_result_activity_setn(md_result_t *result, const char *activity) { result->activity = activity; result->problem = result->detail = NULL; + result->subproblems = NULL; on_change(result); } @@ -91,15 +93,18 @@ void md_result_set(md_result_t *result, apr_status_t status, const char *detail) result->status = status; result->problem = NULL; result->detail = detail? apr_pstrdup(result->p, detail) : NULL; + result->subproblems = NULL; on_change(result); } void md_result_problem_set(md_result_t *result, apr_status_t status, - const char *problem, const char *detail) + const char *problem, const char *detail, + const md_json_t *subproblems) { result->status = status; result->problem = dup_trim(result->p, problem); result->detail = apr_pstrdup(result->p, detail); + result->subproblems = subproblems? md_json_clone(result->p, subproblems) : NULL; on_change(result); } @@ -114,6 +119,7 @@ void md_result_problem_printf(md_result_t *result, apr_status_t status, va_start(ap, fmt); result->detail = apr_pvsprintf(result->p, fmt, ap); va_end(ap); + result->subproblems = NULL; on_change(result); } @@ -125,6 +131,7 @@ void md_result_printf(md_result_t *result, apr_status_t status, const char *fmt, va_start(ap, fmt); result->detail = apr_pvsprintf(result->p, fmt, ap); va_end(ap); + result->subproblems = NULL; on_change(result); } @@ -146,7 +153,7 @@ md_result_t*md_result_from_json(const struct md_json_t *json, apr_pool_t *p) result->activity = md_json_dups(p, json, MD_KEY_ACTIVITY, NULL); s = md_json_dups(p, json, MD_KEY_VALID_FROM, NULL); if (s && *s) result->ready_at = apr_date_parse_rfc(s); - + result->subproblems = md_json_dupj(p, json, MD_KEY_SUBPROBLEMS, NULL); return result; } @@ -169,6 +176,9 @@ struct md_json_t *md_result_to_json(const md_result_t *result, apr_pool_t *p) apr_rfc822_date(ts, result->ready_at); md_json_sets(ts, json, MD_KEY_VALID_FROM, NULL); } + if (result->subproblems) { + md_json_setj(result->subproblems, json, MD_KEY_SUBPROBLEMS, NULL); + } return json; } @@ -200,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->subproblems = src->subproblems; } void md_result_dup(md_result_t *dest, const md_result_t *src) @@ -209,17 +220,18 @@ 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->subproblems = src->subproblems? md_json_clone(dest->p, src->subproblems) : NULL; on_change(dest); } -void md_result_log(md_result_t *result, int level) +void md_result_log(md_result_t *result, unsigned int level) { if (md_log_is_level(result->p, (md_log_level_t)level)) { const char *sep = ""; const char *msg = ""; - if (result->md) { - msg = apr_psprintf(result->p, "md[%s]", result->md->name); + if (result->md_name) { + msg = apr_psprintf(result->p, "md[%s]", result->md_name); sep = " "; } if (result->activity) { @@ -234,6 +246,11 @@ void md_result_log(md_result_t *result, int level) msg = apr_psprintf(result->p, "%s%sdetail[%s]", msg, sep, result->detail); sep = " "; } + if (result->subproblems) { + msg = apr_psprintf(result->p, "%s%ssubproblems[%s]", msg, sep, + md_json_writep(result->subproblems, result->p, MD_JSON_FMT_COMPACT)); + sep = " "; + } md_log_perror(MD_LOG_MARK, (md_log_level_t)level, result->status, result->p, "%s", msg); } } diff --git a/modules/md/md_result.h b/modules/md/md_result.h index 13c5cd2daf..58e903e08a 100644 --- a/modules/md/md_result.h +++ b/modules/md/md_result.h @@ -26,10 +26,11 @@ typedef void md_result_change_cb(md_result_t *result, void *data); struct md_result_t { apr_pool_t *p; - const struct md_t *md; + const char *md_name; apr_status_t status; const char *problem; const char *detail; + const struct md_json_t *subproblems; const char *activity; apr_time_t ready_at; md_result_change_cb *on_change; @@ -37,7 +38,7 @@ struct md_result_t { }; md_result_t *md_result_make(apr_pool_t *p, apr_status_t status); -md_result_t *md_result_md_make(apr_pool_t *p, const struct md_t *md); +md_result_t *md_result_md_make(apr_pool_t *p, const char *md_name); void md_result_reset(md_result_t *result); void md_result_activity_set(md_result_t *result, const char *activity); @@ -46,7 +47,8 @@ void md_result_activity_printf(md_result_t *result, const char *fmt, ...); void md_result_set(md_result_t *result, apr_status_t status, const char *detail); void md_result_problem_set(md_result_t *result, apr_status_t status, - const char *problem, const char *detail); + const char *problem, const char *detail, + const struct md_json_t *subproblems); void md_result_problem_printf(md_result_t *result, apr_status_t status, const char *problem, const char *fmt, ...); @@ -64,7 +66,7 @@ int md_result_cmp(const md_result_t *r1, const md_result_t *r2); void md_result_assign(md_result_t *dest, const md_result_t *src); void md_result_dup(md_result_t *dest, const md_result_t *src); -void md_result_log(md_result_t *result, int level); +void md_result_log(md_result_t *result, unsigned int level); void md_result_on_change(md_result_t *result, md_result_change_cb *cb, void *data); diff --git a/modules/md/md_status.c b/modules/md/md_status.c index 4bdd508199..3c3d801400 100644 --- a/modules/md/md_status.c +++ b/modules/md/md_status.c @@ -27,6 +27,7 @@ #include "md.h" #include "md_crypt.h" #include "md_log.h" +#include "md_ocsp.h" #include "md_store.h" #include "md_result.h" #include "md_reg.h" @@ -40,16 +41,15 @@ static apr_status_t status_get_cert_json(md_json_t **pjson, const md_cert_t *cert, apr_pool_t *p) { - char ts[APR_RFC822_DATE_LEN]; const char *finger; apr_status_t rv = APR_SUCCESS; + md_timeperiod_t valid; md_json_t *json; json = md_json_create(p); - apr_rfc822_date(ts, md_cert_get_not_before(cert)); - md_json_sets(ts, json, MD_KEY_VALID_FROM, NULL); - apr_rfc822_date(ts, md_cert_get_not_after(cert)); - md_json_sets(ts, json, MD_KEY_VALID_UNTIL, NULL); + valid.start = md_cert_get_not_before(cert); + valid.end = md_cert_get_not_after(cert); + md_json_set_timeperiod(&valid, json, MD_KEY_VALID, NULL); md_json_sets(md_cert_get_serial_number(cert, p), json, MD_KEY_SERIAL, NULL); if (APR_SUCCESS != (rv = md_cert_to_sha256_fingerprint(&finger, cert, p))) goto leave; md_json_sets(finger, json, MD_KEY_SHA256_FINGERPRINT, NULL); @@ -102,7 +102,7 @@ static apr_status_t get_staging_cert_json(md_json_t **pjson, apr_pool_t *p, rv = APR_SUCCESS; goto leave; } - else if (APR_SUCCESS != rv) { + else if (APR_SUCCESS != rv || certs->nelts == 0) { goto leave; } cert = APR_ARRAY_IDX(certs, 0, md_cert_t *); @@ -112,52 +112,88 @@ leave: return rv; } -static apr_status_t job_loadj(md_json_t **pjson, const char *name, - struct md_reg_t *reg, apr_pool_t *p) +static apr_status_t job_loadj(md_json_t **pjson, md_store_group_t group, const char *name, + struct md_reg_t *reg, int with_log, apr_pool_t *p) { + apr_status_t rv; + md_store_t *store = md_reg_store_get(reg); - return md_store_load_json(store, MD_SG_STAGING, name, MD_FN_JOB, pjson, p); + rv = md_store_load_json(store, group, name, MD_FN_JOB, pjson, p); + if (APR_SUCCESS == rv && !with_log) md_json_del(*pjson, MD_KEY_LOG, NULL); + return rv; } -apr_status_t md_status_get_md_json(md_json_t **pjson, const md_t *md, - md_reg_t *reg, apr_pool_t *p) +static apr_status_t status_get_md_json(md_json_t **pjson, const md_t *md, + md_reg_t *reg, md_ocsp_reg_t *ocsp, + int with_logs, apr_pool_t *p) { md_json_t *mdj, *jobj, *certj; int renew; const md_pubcert_t *pubcert; - const md_cert_t *cert; + const md_cert_t *cert = NULL; + md_ocsp_cert_stat_t cert_stat; + md_timeperiod_t ocsp_valid; apr_status_t rv = APR_SUCCESS; + apr_time_t renew_at; mdj = md_to_json(md, p); if (APR_SUCCESS == md_reg_get_pubcert(&pubcert, reg, md, p)) { cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*); if (APR_SUCCESS != (rv = status_get_cert_json(&certj, cert, p))) goto leave; + if (md->stapling && ocsp) { + rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md); + if (APR_SUCCESS == rv) { + md_json_sets(md_ocsp_cert_stat_name(cert_stat), certj, MD_KEY_OCSP, MD_KEY_STATUS, NULL); + md_json_set_timeperiod(&ocsp_valid, certj, MD_KEY_OCSP, MD_KEY_VALID, NULL); + } + else if (!APR_STATUS_IS_ENOENT(rv)) goto leave; + rv = APR_SUCCESS; + if (APR_SUCCESS == job_loadj(&jobj, MD_SG_OCSP, md->name, reg, with_logs, p)) { + md_json_setj(jobj, certj, MD_KEY_OCSP, MD_KEY_RENEWAL, NULL); + } + } md_json_setj(certj, mdj, MD_KEY_CERT, NULL); + + renew_at = md_reg_renew_at(reg, md, p); + if (renew_at) { + md_json_set_time(renew_at, mdj, MD_KEY_RENEW_AT, NULL); + } } + md_json_setb(md->stapling, mdj, MD_KEY_STAPLING, NULL); + md_json_setb(md->watched, mdj, MD_KEY_WATCHED, NULL); renew = md_reg_should_renew(reg, md, p); - md_json_setb(renew, mdj, MD_KEY_RENEW, NULL); if (renew) { - rv = job_loadj(&jobj, md->name, reg, p); + md_json_setb(renew, mdj, MD_KEY_RENEW, NULL); + rv = job_loadj(&jobj, MD_SG_STAGING, md->name, reg, with_logs, p); if (APR_SUCCESS == rv) { - rv = get_staging_cert_json(&certj, p, reg, md); - if (APR_SUCCESS != rv) goto leave; - if (certj) md_json_setj(certj, jobj, MD_KEY_CERT, NULL); + if (APR_SUCCESS == get_staging_cert_json(&certj, p, reg, md)) { + md_json_setj(certj, jobj, MD_KEY_CERT, NULL); + } md_json_setj(jobj, mdj, MD_KEY_RENEWAL, NULL); } else if (APR_STATUS_IS_ENOENT(rv)) rv = APR_SUCCESS; else goto leave; } + leave: - *pjson = (APR_SUCCESS == rv)? mdj : NULL; + if (APR_SUCCESS != rv) { + md_json_setl(rv, mdj, MD_KEY_ERROR, NULL); + } + *pjson = mdj; return rv; } +apr_status_t md_status_get_md_json(md_json_t **pjson, const md_t *md, + md_reg_t *reg, md_ocsp_reg_t *ocsp, apr_pool_t *p) +{ + return status_get_md_json(pjson, md, reg, ocsp, 1, p); +} + apr_status_t md_status_get_json(md_json_t **pjson, apr_array_header_t *mds, - md_reg_t *reg, apr_pool_t *p) + md_reg_t *reg, md_ocsp_reg_t *ocsp, apr_pool_t *p) { md_json_t *json, *mdj; - apr_status_t rv = APR_SUCCESS; const md_t *md; int i; @@ -165,33 +201,41 @@ apr_status_t md_status_get_json(md_json_t **pjson, apr_array_header_t *mds, md_json_sets(MOD_MD_VERSION, json, MD_KEY_VERSION, NULL); for (i = 0; i < mds->nelts; ++i) { md = APR_ARRAY_IDX(mds, i, const md_t *); - rv = md_status_get_md_json(&mdj, md, reg, p); - if (APR_SUCCESS != rv) goto leave; + status_get_md_json(&mdj, md, reg, ocsp, 0, p); md_json_addj(mdj, json, MD_KEY_MDS, NULL); } -leave: - *pjson = (APR_SUCCESS == rv)? json : NULL; - return rv; + *pjson = json; + return APR_SUCCESS; } /**************************************************************************************************/ /* drive job persistence */ -md_job_t *md_job_make(apr_pool_t *p, const char *name) +md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, + md_store_group_t group, const char *name) { md_job_t *job = apr_pcalloc(p, sizeof(*job)); - job->name = apr_pstrdup(p, name); + job->group = group; + job->mdomain = apr_pstrdup(p, name); + job->store = store; job->p = p; + job->max_log = 128; return job; } +void md_job_set_group(md_job_t *job, md_store_group_t group) +{ + job->group = group; +} + static void md_job_from_json(md_job_t *job, md_json_t *json, apr_pool_t *p) { const char *s; /* not good, this is malloced from a temp pool */ - /*job->name = md_json_gets(json, MD_KEY_NAME, NULL);*/ + /*job->mdomain = md_json_gets(json, MD_KEY_NAME, NULL);*/ job->finished = md_json_getb(json, MD_KEY_FINISHED, NULL); + job->notified = md_json_getb(json, MD_KEY_NOTIFIED, NULL); s = md_json_dups(p, json, MD_KEY_NEXT_RUN, NULL); if (s && *s) job->next_run = apr_date_parse_rfc(s); s = md_json_dups(p, json, MD_KEY_LAST_RUN, NULL); @@ -210,8 +254,9 @@ static void job_to_json(md_json_t *json, const md_job_t *job, { char ts[APR_RFC822_DATE_LEN]; - md_json_sets(job->name, json, MD_KEY_NAME, NULL); + md_json_sets(job->mdomain, json, MD_KEY_NAME, NULL); md_json_setb(job->finished, json, MD_KEY_FINISHED, NULL); + md_json_setb(job->notified, json, MD_KEY_NOTIFIED, NULL); if (job->next_run > 0) { apr_rfc822_date(ts, job->next_run); md_json_sets(ts, json, MD_KEY_NEXT_RUN, NULL); @@ -232,31 +277,27 @@ static void job_to_json(md_json_t *json, const md_job_t *job, if (job->log) md_json_setj(job->log, json, MD_KEY_LOG, NULL); } -apr_status_t md_job_load(md_job_t *job, md_reg_t *reg, - md_store_group_t group, apr_pool_t *p) +apr_status_t md_job_load(md_job_t *job) { - md_store_t *store = md_reg_store_get(reg); md_json_t *jprops; apr_status_t rv; - rv = md_store_load_json(store, group, job->name, MD_FN_JOB, &jprops, p); + rv = md_store_load_json(job->store, job->group, job->mdomain, MD_FN_JOB, &jprops, job->p); if (APR_SUCCESS == rv) { - md_job_from_json(job, jprops, p); + md_job_from_json(job, jprops, job->p); } return rv; } -apr_status_t md_job_save(md_job_t *job, struct md_reg_t *reg, - md_store_group_t group, md_result_t *result, - apr_pool_t *p) +apr_status_t md_job_save(md_job_t *job, md_result_t *result, apr_pool_t *p) { - md_store_t *store = md_reg_store_get(reg); md_json_t *jprops; apr_status_t rv; jprops = md_json_create(p); job_to_json(jprops, job, result, p); - rv = md_store_save_json(store, p, group, job->name, MD_FN_JOB, jprops, 0); + rv = md_store_save_json(job->store, p, job->group, job->mdomain, MD_FN_JOB, jprops, 0); + if (APR_SUCCESS == rv) job->dirty = 0; return rv; } @@ -274,6 +315,8 @@ void md_job_log_append(md_job_t *job, const char *type, if (detail) md_json_sets(detail, entry, MD_KEY_DETAIL, NULL); if (!job->log) job->log = md_json_create(job->p); md_json_insertj(entry, 0, job->log, MD_KEY_ENTRIES, NULL); + md_json_limita(job->max_log, job->log, MD_KEY_ENTRIES, NULL); + job->dirty = 1; } typedef struct { @@ -301,9 +344,10 @@ md_json_t *md_job_log_get_latest(md_job_t *job, const char *type) { log_find_ctx ctx; + + memset(&ctx, 0, sizeof(ctx)); ctx.job = job; ctx.type = type; - memset(&ctx, 0, sizeof(ctx)); if (job->log) md_json_itera(find_first_log_entry, &ctx, job->log, MD_KEY_ENTRIES, NULL); return ctx.entry; } @@ -325,7 +369,7 @@ void md_status_take_stock(md_json_t **pjson, apr_array_header_t *mds, md_reg_t *reg, apr_pool_t *p) { const md_t *md; - md_job_t job; + md_job_t *job; int i, complete, renewing, errored, ready, total; md_json_t *json; @@ -339,14 +383,13 @@ void md_status_take_stock(md_json_t **pjson, apr_array_header_t *mds, case MD_S_INCOMPLETE: if (md_reg_should_renew(reg, md, p)) { ++renewing; - memset(&job, 0, sizeof(job)); - job.name = md->name; - if (APR_SUCCESS == md_job_load(&job, reg, MD_SG_STAGING, p)) { - if (job.error_runs > 0 - || (job.last_result && job.last_result->status != APR_SUCCESS)) { + job = md_reg_job_make(reg, md->name, p); + if (APR_SUCCESS == md_job_load(job)) { + if (job->error_runs > 0 + || (job->last_result && job->last_result->status != APR_SUCCESS)) { ++errored; } - else if (job.finished) { + else if (job->finished) { ++ready; } } @@ -362,3 +405,142 @@ void md_status_take_stock(md_json_t **pjson, apr_array_header_t *mds, md_json_setl(ready, json, MD_KEY_READY, NULL); *pjson = json; } + +typedef struct { + apr_pool_t *p; + md_job_t *job; + md_store_t *store; + md_result_t *last; + apr_time_t last_save; +} md_job_result_ctx; + +static void job_result_update(md_result_t *result, void *data) +{ + md_job_result_ctx *ctx = data; + apr_time_t now; + const char *msg, *sep; + + if (md_result_cmp(ctx->last, result)) { + now = apr_time_now(); + md_result_assign(ctx->last, result); + if (result->activity || result->problem || result->detail) { + msg = sep = ""; + if (result->activity) { + msg = apr_psprintf(result->p, "%s", result->activity); + sep = ": "; + } + if (result->detail) { + msg = apr_psprintf(result->p, "%s%s%s", msg, sep, result->detail); + sep = ", "; + } + if (result->problem) { + msg = apr_psprintf(result->p, "%s%sproblem: %s", msg, sep, result->problem); + sep = " "; + } + md_job_log_append(ctx->job, "progress", NULL, msg); + + if (ctx->store && apr_time_as_msec(now - ctx->last_save) > 500) { + md_job_save(ctx->job, result, ctx->p); + ctx->last_save = now; + } + } + } +} + +static void job_observation_start(md_job_t *job, md_result_t *result, md_store_t *store) +{ + md_job_result_ctx *ctx; + + if (job->observing) md_result_on_change(job->observing, NULL, NULL); + job->observing = result; + + ctx = apr_pcalloc(result->p, sizeof(*ctx)); + ctx->p = result->p; + ctx->job = job; + ctx->store = store; + ctx->last = md_result_md_make(result->p, APR_SUCCESS); + md_result_assign(ctx->last, result); + md_result_on_change(result, job_result_update, ctx); +} + +static void job_observation_end(md_job_t *job) +{ + if (job->observing) md_result_on_change(job->observing, NULL, NULL); + job->observing = NULL; +} + +void md_job_start_run(md_job_t *job, md_result_t *result, md_store_t *store) +{ + job->fatal_error = 0; + job->last_run = apr_time_now(); + job_observation_start(job, result, store); + md_job_log_append(job, "starting", NULL, NULL); +} + +apr_time_t md_job_delay_on_errors(int err_count) +{ + apr_time_t delay = 0; + + if (err_count > 0) { + /* back off duration, depending on the errors we encounter in a row */ + delay = apr_time_from_sec(5 << (err_count - 1)); + if (delay > apr_time_from_sec(60*60)) { + delay = apr_time_from_sec(60*60); + } + } + return delay; +} + +void md_job_end_run(md_job_t *job, md_result_t *result) +{ + if (APR_SUCCESS == result->status) { + job->finished = 1; + job->valid_from = result->ready_at; + job->error_runs = 0; + job->dirty = 1; + md_job_log_append(job, "finished", NULL, NULL); + } + else { + ++job->error_runs; + job->dirty = 1; + job->next_run = apr_time_now() + md_job_delay_on_errors(job->error_runs); + } + job_observation_end(job); +} + +void md_job_retry_at(md_job_t *job, apr_time_t later) +{ + job->next_run = later; + job->dirty = 1; +} + +apr_status_t md_job_notify(md_job_t *job, const char *reason, md_result_t *result) +{ + if (job->notify) return job->notify(job, reason, result, job->p, job->notify_ctx); + job->dirty = 1; + if (APR_SUCCESS == result->status) { + job->notified = 1; + job->error_runs = 0; + } + else { + ++job->error_runs; + job->next_run = apr_time_now() + md_job_delay_on_errors(job->error_runs); + } + return result->status; +} + +void md_job_holler(md_job_t *job, const char *reason) +{ + md_result_t *result; + + if (job->notify) { + result = md_result_make(job->p, APR_SUCCESS); + job->notify(job, reason, result, job->p, job->notify_ctx); + } +} + +void md_job_set_notify_cb(md_job_t *job, md_job_notify_cb *cb, void *baton) +{ + job->notify = cb; + job->notify_ctx = baton; +} diff --git a/modules/md/md_status.h b/modules/md/md_status.h index c74d680573..ac25ccda7d 100644 --- a/modules/md/md_status.h +++ b/modules/md/md_status.h @@ -20,18 +20,23 @@ struct md_json_t; struct md_reg_t; struct md_result_t; +struct md_ocsp_reg_t; + +#include "md_store.h" /** * Get a JSON summary of the MD and its status (certificates, jobs, etc.). */ apr_status_t md_status_get_md_json(struct md_json_t **pjson, const md_t *md, - struct md_reg_t *reg, apr_pool_t *p); + struct md_reg_t *reg, struct md_ocsp_reg_t *ocsp, + apr_pool_t *p); /** * Get a JSON summary of all MDs and their status. */ apr_status_t md_status_get_json(struct md_json_t **pjson, apr_array_header_t *mds, - struct md_reg_t *reg, apr_pool_t *p); + struct md_reg_t *reg, struct md_ocsp_reg_t *ocsp, + apr_pool_t *p); /** * Take stock of all MDs given for a short overview. The JSON returned @@ -41,38 +46,50 @@ apr_status_t md_status_get_json(struct md_json_t **pjson, apr_array_header_t *md void md_status_take_stock(struct md_json_t **pjson, apr_array_header_t *mds, struct md_reg_t *reg, apr_pool_t *p); + typedef struct md_job_t md_job_t; + struct md_job_t { - const char *name; /* Name of the MD this job is about */ + md_store_group_t group;/* group where job is persisted */ + const char *mdomain; /* Name of the MD this job is about */ + md_store_t *store; /* store where it is persisted */ apr_pool_t *p; apr_time_t next_run; /* Time this job wants to be processed next */ apr_time_t last_run; /* Time this job ran last (or 0) */ struct md_result_t *last_result; /* Result from last run */ int finished; /* true iff the job finished successfully */ + int notified; /* true iff notifications were handled successfully */ apr_time_t valid_from; /* at which time the finished job results become valid, 0 if immediate */ int error_runs; /* Number of errored runs of an unfinished job */ + int fatal_error; /* a fatal error is remedied by retrying */ md_json_t *log; /* array of log objects with minimum fields - MD_KEY_WHEN (timestamp) and MD_KEY_TYPE (string) */ + MD_KEY_WHEN (timestamp) and MD_KEY_TYPE (string) */ + apr_size_t max_log; /* max number of log entries, new ones replace oldest */ + int dirty; + struct md_result_t *observing; + + md_job_notify_cb *notify; + void *notify_ctx; }; /** - * Create a new job instance for the given MD name. Job load/save will work - * on the MD_SG_STAGING for the name. + * Create a new job instance for the given MD name. + * Job load/save will work using the name. */ -md_job_t *md_job_make(apr_pool_t *p, const char *name); +md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, + md_store_group_t group, const char *name); + +void md_job_set_group(md_job_t *job, md_store_group_t group); /** - * Update the job from storage in <group>/job->name. + * Update the job from storage in <group>/job->mdomain. */ -apr_status_t md_job_load(md_job_t *job, struct md_reg_t *reg, - md_store_group_t group, apr_pool_t *p); +apr_status_t md_job_load(md_job_t *job); /** - * Update storage from job in <group>/job->name. + * Update storage from job in <group>/job->mdomain. */ -apr_status_t md_job_save(md_job_t *job, struct md_reg_t *reg, - md_store_group_t group, struct md_result_t *result, - apr_pool_t *p); +apr_status_t md_job_save(md_job_t *job, struct md_result_t *result, apr_pool_t *p); /** * Append to the job's log. Timestamp is automatically added. @@ -94,4 +111,16 @@ md_json_t *md_job_log_get_latest(md_job_t *job, const char *type); */ apr_time_t md_job_log_get_time_of_latest(md_job_t *job, const char *type); +void md_job_start_run(md_job_t *job, struct md_result_t *result, md_store_t *store); +void md_job_end_run(md_job_t *job, struct md_result_t *result); +void md_job_retry_at(md_job_t *job, apr_time_t later); + +/* Given the number of errors encountered, recommend a delay for the next attempt */ +apr_time_t md_job_delay_on_errors(int err_count); + +void md_job_set_notify_cb(md_job_t *job, md_job_notify_cb *cb, void *baton); +apr_status_t md_job_notify(md_job_t *job, const char *reason, struct md_result_t *result); +/* Same as notify but without checks on success and no change to job */ +void md_job_holler(md_job_t *job, const char *reason); + #endif /* md_status_h */ diff --git a/modules/md/md_store.c b/modules/md/md_store.c index c66890eec0..ad5bb28cf2 100644 --- a/modules/md/md_store.c +++ b/modules/md/md_store.c @@ -55,22 +55,18 @@ static const char *GROUP_NAME[] = { "staging", "archive", "tmp", + "ocsp", NULL }; -const char *md_store_group_name(int group) +const char *md_store_group_name(unsigned int group) { - if ((size_t)group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) { + if (group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) { return GROUP_NAME[group]; } return "UNKNOWN"; } -void md_store_destroy(md_store_t *store) -{ - if (store->destroy) store->destroy(store); -} - apr_status_t md_store_load(md_store_t *store, md_store_group_t group, const char *name, const char *aspect, md_store_vtype_t vtype, void **pdata, @@ -145,12 +141,33 @@ int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group return store->is_newer(store, group1, group2, name, aspect, p); } +apr_time_t md_store_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p) +{ + return store->get_modified(store, group, name, aspect, p); +} + apr_status_t md_store_iter_names(md_store_inspect *inspect, void *baton, md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *pattern) { return store->iterate_names(inspect, baton, store, p, group, pattern); } +apr_status_t md_store_remove_not_modified_since(md_store_t *store, apr_pool_t *p, + apr_time_t modified, + md_store_group_t group, + const char *name, + const char *aspect) +{ + return store->remove_nms(store, p, modified, group, name, aspect); +} + +apr_status_t md_store_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, const char *to) +{ + return store->rename(store, p, group, name, to); +} + /**************************************************************************************************/ /* convenience */ diff --git a/modules/md/md_store.h b/modules/md/md_store.h index 7dec9f39fe..dfe9f32d6d 100644 --- a/modules/md/md_store.h +++ b/modules/md/md_store.h @@ -21,107 +21,183 @@ struct apr_array_header_t; struct md_cert_t; struct md_pkey_t; -typedef struct md_store_t md_store_t; - -typedef void md_store_destroy_cb(md_store_t *store); - -const char *md_store_group_name(int group); +const char *md_store_group_name(unsigned int group); +typedef struct md_store_t md_store_t; -typedef apr_status_t md_store_load_cb(md_store_t *store, md_store_group_t group, - const char *name, const char *aspect, - md_store_vtype_t vtype, void **pvalue, - apr_pool_t *p); -typedef apr_status_t md_store_save_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, - const char *name, const char *aspect, - md_store_vtype_t vtype, void *value, - int create); -typedef apr_status_t md_store_remove_cb(md_store_t *store, md_store_group_t group, - const char *name, const char *aspect, - apr_pool_t *p, int force); -typedef apr_status_t md_store_purge_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, - const char *name); - -typedef int md_store_inspect(void *baton, const char *name, const char *aspect, - md_store_vtype_t vtype, void *value, apr_pool_t *ptemp); - -typedef apr_status_t md_store_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, - apr_pool_t *p, md_store_group_t group, const char *pattern, - const char *aspect, md_store_vtype_t vtype); - -typedef apr_status_t md_store_names_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, - apr_pool_t *p, md_store_group_t group, const char *pattern); - -typedef apr_status_t md_store_move_cb(md_store_t *store, apr_pool_t *p, md_store_group_t from, - md_store_group_t to, const char *name, int archive); - -typedef apr_status_t md_store_get_fname_cb(const char **pfname, - md_store_t *store, md_store_group_t group, - const char *name, const char *aspect, - apr_pool_t *p); - -typedef int md_store_is_newer_cb(md_store_t *store, - md_store_group_t group1, md_store_group_t group2, - const char *name, const char *aspect, apr_pool_t *p); - -struct md_store_t { - md_store_destroy_cb *destroy; - - md_store_save_cb *save; - md_store_load_cb *load; - md_store_remove_cb *remove; - md_store_move_cb *move; - md_store_iter_cb *iterate; - md_store_names_iter_cb *iterate_names; - md_store_purge_cb *purge; - md_store_get_fname_cb *get_fname; - md_store_is_newer_cb *is_newer; -}; - -void md_store_destroy(md_store_t *store); +/** + * A store for domain related data. + * + * The Key for a piece of data is the set of 3 items + * <group> + <domain> + <aspect> + * + * Examples: + * "domains" + "greenbytes.de" + "pubcert.pem" + * "ocsp" + "greenbytes.de" + "ocsp-XXXXX.json" + * + * Storage groups are pre-defined, domain and aspect names can be freely chosen. + * + * Groups reflect use cases and come with security restrictions. The groups + * DOMAINS, ARCHIVE and NONE are only accessible during the startup + * phase of httpd. + * + * Private key are stored unencrypted only in restricted groups. Meaning that certificate + * keys in group DOMAINS are not encrypted, but only readable at httpd start/reload. + * Keys in unrestricted groups are encrypted using a pass phrase generated once and stored + * in NONE. + */ +/** Value types handled by a store */ +typedef enum { + MD_SV_TEXT, /* plain text, value is (char*) */ + MD_SV_JSON, /* JSON serialization, value is (md_json_t*) */ + MD_SV_CERT, /* PEM x509 certificate, value is (md_cert_t*) */ + MD_SV_PKEY, /* PEM private key, value is (md_pkey_t*) */ + MD_SV_CHAIN, /* list of PEM x509 certificates, value is + (apr_array_header_t*) of (md_cert*) */ +} md_store_vtype_t; + +/** Store storage groups */ +typedef enum { + MD_SG_NONE, /* top level of store, name MUST be NULL in calls */ + MD_SG_ACCOUNTS, /* ACME accounts */ + MD_SG_CHALLENGES, /* challenge response data for a domain */ + MD_SG_DOMAINS, /* live certificates and settings for a domain */ + MD_SG_STAGING, /* staged set of certificate and settings, maybe incomplete */ + MD_SG_ARCHIVE, /* Archived live sets of a domain */ + MD_SG_TMP, /* temporary domain storage */ + MD_SG_OCSP, /* OCSP stapling related domain data */ + MD_SG_COUNT, /* number of storage groups, used in setups */ +} md_store_group_t; + +#define MD_FN_MD "md.json" +#define MD_FN_JOB "job.json" +#define MD_FN_PRIVKEY "privkey.pem" +#define MD_FN_PUBCERT "pubcert.pem" +#define MD_FN_CERT "cert.pem" +#define MD_FN_HTTPD_JSON "httpd.json" + +#define MD_FN_FALLBACK_PKEY "fallback-privkey.pem" +#define MD_FN_FALLBACK_CERT "fallback-cert.pem" + +/** + * Load the JSON value at key "group/name/aspect", allocated from pool p. + * @return APR_ENOENT if there is no such value + */ apr_status_t md_store_load_json(md_store_t *store, md_store_group_t group, const char *name, const char *aspect, struct md_json_t **pdata, apr_pool_t *p); +/** + * Save the JSON value at key "group/name/aspect". If create != 0, fail if there + * already is a value for this key. + */ apr_status_t md_store_save_json(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name, const char *aspect, struct md_json_t *data, int create); - +/** + * Load the value of type at key "group/name/aspect", allocated from pool p. Usually, the + * type is expected to be the same as used in saving the value. Some conversions will work, + * others will fail the format. + * @return APR_ENOENT if there is no such value + */ apr_status_t md_store_load(md_store_t *store, md_store_group_t group, const char *name, const char *aspect, md_store_vtype_t vtype, void **pdata, apr_pool_t *p); +/** + * Save the JSON value at key "group/name/aspect". If create != 0, fail if there + * already is a value for this key. The provided data MUST be of the correct type. + */ apr_status_t md_store_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name, const char *aspect, md_store_vtype_t vtype, void *data, int create); + +/** + * Remove the value stored at key "group/name/aspect". Unless force != 0, a missing + * value will cause the call to fail with APR_ENOENT. + */ apr_status_t md_store_remove(md_store_t *store, md_store_group_t group, const char *name, const char *aspect, apr_pool_t *p, int force); +/** + * Remove everything matching key "group/name". + */ apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name); +/** + * Remove all items matching the name/aspect patterns that have not been + * modified since the given timestamp. + */ +apr_status_t md_store_remove_not_modified_since(md_store_t *store, apr_pool_t *p, + apr_time_t modified, + md_store_group_t group, + const char *name, + const char *aspect); + +/** + * inspect callback function. Invoked for each matched value. Values allocated from + * ptemp may disappear any time after the call returned. If this function returns + * 0, the iteration is aborted. + */ +typedef int md_store_inspect(void *baton, const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp); +/** + * Iterator over all existing values matching the name pattern. Patterns are evaluated + * using apr_fnmatch() without flags. + */ apr_status_t md_store_iter(md_store_inspect *inspect, void *baton, md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *pattern, const char *aspect, md_store_vtype_t vtype); +/** + * Move everything matching key "from/name" from one group to another. If archive != 0, + * move any existing "to/name" into a new "archive/new_name" location. + */ apr_status_t md_store_move(md_store_t *store, apr_pool_t *p, md_store_group_t from, md_store_group_t to, const char *name, int archive); +/** + * Rename a group member. + */ +apr_status_t md_store_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, const char *to); + +/** + * Get the filename of an item stored in "group/name/aspect". The item does + * not have to exist. + */ apr_status_t md_store_get_fname(const char **pfname, md_store_t *store, md_store_group_t group, const char *name, const char *aspect, apr_pool_t *p); +/** + * Make a compare on the modification time of "group1/name/aspect" vs. "group2/name/aspect". + */ int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, const char *name, const char *aspect, apr_pool_t *p); +/** + * Iterate over all names that exist in a group, e.g. there are items matching + * "group/pattern". The inspect function is called with the name and NULL aspect + * and value. + */ apr_status_t md_store_iter_names(md_store_inspect *inspect, void *baton, md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *pattern); +/** + * Get the modification time of the item store under "group/name/aspect". + * @return modification time or 0 if the item does not exist. + */ +apr_time_t md_store_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p); + + /**************************************************************************************************/ /* Storage handling utils */ @@ -153,5 +229,66 @@ apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name, struct apr_array_header_t *pubcert, int create); +/**************************************************************************************************/ +/* implementation interface */ + +typedef apr_status_t md_store_load_cb(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void **pvalue, + apr_pool_t *p); +typedef apr_status_t md_store_save_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, + int create); +typedef apr_status_t md_store_remove_cb(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p, int force); +typedef apr_status_t md_store_purge_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name); + +typedef apr_status_t md_store_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern, + const char *aspect, md_store_vtype_t vtype); + +typedef apr_status_t md_store_names_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern); + +typedef apr_status_t md_store_move_cb(md_store_t *store, apr_pool_t *p, md_store_group_t from, + md_store_group_t to, const char *name, int archive); + +typedef apr_status_t md_store_rename_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *from, const char *to); + +typedef apr_status_t md_store_get_fname_cb(const char **pfname, + md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p); + +typedef int md_store_is_newer_cb(md_store_t *store, + md_store_group_t group1, md_store_group_t group2, + const char *name, const char *aspect, apr_pool_t *p); + +typedef apr_time_t md_store_get_modified_cb(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p); + +typedef apr_status_t md_store_remove_nms_cb(md_store_t *store, apr_pool_t *p, + apr_time_t modified, md_store_group_t group, + const char *name, const char *aspect); + +struct md_store_t { + md_store_save_cb *save; + md_store_load_cb *load; + md_store_remove_cb *remove; + md_store_move_cb *move; + md_store_rename_cb *rename; + md_store_iter_cb *iterate; + md_store_names_iter_cb *iterate_names; + md_store_purge_cb *purge; + md_store_get_fname_cb *get_fname; + md_store_is_newer_cb *is_newer; + md_store_get_modified_cb *get_modified; + md_store_remove_nms_cb *remove_nms; +}; + #endif /* mod_md_md_store_h */ diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c index 04c7e60557..6553fdd706 100644 --- a/modules/md/md_store_fs.c +++ b/modules/md/md_store_fs.c @@ -55,8 +55,7 @@ struct md_store_fs_t { md_store_fs_cb *event_cb; void *event_baton; - const unsigned char *key; - apr_size_t key_len; + md_data_t key; int plain_pkey[MD_SG_COUNT]; int port_80; @@ -78,9 +77,14 @@ static apr_status_t fs_remove(md_store_t *store, md_store_group_t group, apr_pool_t *p, int force); static apr_status_t fs_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name); +static apr_status_t fs_remove_nms(md_store_t *store, apr_pool_t *p, + apr_time_t modified, md_store_group_t group, + const char *name, const char *aspect); static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, md_store_group_t from, md_store_group_t to, const char *name, int archive); +static apr_status_t fs_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *from, const char *to); static apr_status_t fs_iterate(md_store_inspect *inspect, void *baton, md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *pattern, const char *aspect, md_store_vtype_t vtype); @@ -94,23 +98,25 @@ static apr_status_t fs_get_fname(const char **pfname, static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, const char *name, const char *aspect, apr_pool_t *p); +static apr_time_t fs_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p); + static apr_status_t init_store_file(md_store_fs_t *s_fs, const char *fname, apr_pool_t *p, apr_pool_t *ptemp) { md_json_t *json = md_json_create(p); const char *key64; - unsigned char *key; apr_status_t rv; md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL); - s_fs->key_len = FS_STORE_KLEN; - s_fs->key = key = apr_pcalloc(p, FS_STORE_KLEN); - if (APR_SUCCESS != (rv = md_rand_bytes(key, s_fs->key_len, p))) { + s_fs->key.len = FS_STORE_KLEN; + s_fs->key.data = apr_pcalloc(p, FS_STORE_KLEN); + if (APR_SUCCESS != (rv = md_rand_bytes((unsigned char*)s_fs->key.data, s_fs->key.len, p))) { return rv; } - key64 = md_util_base64url_encode((char *)key, s_fs->key_len, ptemp); + key64 = md_util_base64url_encode(&s_fs->key, ptemp); md_json_sets(key64, json, MD_KEY_KEY, NULL); rv = md_json_fcreatex(json, ptemp, MD_JSON_FMT_INDENT, fname, MD_FPROT_F_UONLY); memset((char*)key64, 0, strlen(key64)); @@ -193,7 +199,7 @@ static apr_status_t read_store_file(md_store_fs_t *s_fs, const char *fname, apr_pool_t *p, apr_pool_t *ptemp) { md_json_t *json; - const char *key64, *key; + const char *key64; apr_status_t rv; double store_version; @@ -214,11 +220,10 @@ static apr_status_t read_store_file(md_store_fs_t *s_fs, const char *fname, return APR_EINVAL; } - s_fs->key_len = md_util_base64url_decode(&key, key64, p); - s_fs->key = (const unsigned char*)key; - if (s_fs->key_len != FS_STORE_KLEN) { + md_util_base64url_decode(&s_fs->key, key64, p); + if (s_fs->key.len != FS_STORE_KLEN) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "key length unexpected: %" APR_SIZE_T_FMT, - s_fs->key_len); + s_fs->key.len); return APR_EINVAL; } @@ -279,11 +284,14 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa s_fs->s.save = fs_save; s_fs->s.remove = fs_remove; s_fs->s.move = fs_move; + s_fs->s.rename = fs_rename; s_fs->s.purge = fs_purge; s_fs->s.iterate = fs_iterate; s_fs->s.iterate_names = fs_iterate_names; s_fs->s.get_fname = fs_get_fname; s_fs->s.is_newer = fs_is_newer; + s_fs->s.get_modified = fs_get_modified; + s_fs->s.remove_nms = fs_remove_nms; /* by default, everything is only readable by the current user */ s_fs->def_perms.dir = MD_FPROT_D_UONLY; @@ -298,6 +306,9 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa /* challenges dir and files are readable by all, no secrets involved */ s_fs->group_perms[MD_SG_CHALLENGES].dir = MD_FPROT_D_UALL_WREAD; s_fs->group_perms[MD_SG_CHALLENGES].file = MD_FPROT_F_UALL_WREAD; + /* OCSP data is readable by all, no secrets involved */ + s_fs->group_perms[MD_SG_OCSP].dir = MD_FPROT_D_UALL_WREAD; + s_fs->group_perms[MD_SG_OCSP].file = MD_FPROT_F_UALL_WREAD; s_fs->base = apr_pstrdup(p, path); @@ -392,8 +403,8 @@ static void get_pass(const char **ppass, apr_size_t *plen, *plen = 0; } else { - *ppass = (const char *)s_fs->key; - *plen = s_fs->key_len; + *ppass = (const char *)s_fs->key.data; + *plen = s_fs->key.len; } } @@ -522,7 +533,6 @@ static apr_status_t pfs_is_newer(void *baton, apr_pool_t *p, apr_pool_t *ptemp, return rv; } - static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, const char *name, const char *aspect, apr_pool_t *p) { @@ -537,6 +547,44 @@ static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_grou return 0; } +static apr_status_t pfs_get_modified(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *fname, *name, *aspect; + md_store_group_t group; + apr_finfo_t inf; + apr_time_t *pmtime; + apr_status_t rv; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + aspect = va_arg(ap, const char*); + pmtime = va_arg(ap, apr_time_t*); + + *pmtime = 0; + if ( MD_OK(fs_get_fname(&fname, &s_fs->s, group, name, aspect, ptemp)) + && MD_OK(apr_stat(&inf, fname, APR_FINFO_MTIME, ptemp))) { + *pmtime = inf.mtime; + } + + return rv; +} + +static apr_time_t fs_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p) +{ + md_store_fs_t *s_fs = FS_STORE(store); + apr_time_t mtime; + apr_status_t rv; + + rv = md_util_pool_vdo(pfs_get_modified, s_fs, p, group, name, aspect, &mtime, NULL); + if (APR_SUCCESS == rv) { + return mtime; + } + return 0; +} + static apr_status_t pfs_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) { md_store_fs_t *s_fs = baton; @@ -700,6 +748,7 @@ typedef struct { md_store_inspect *inspect; const char *dirname; void *baton; + apr_time_t ts; } inspect_ctx; static apr_status_t insp(void *baton, apr_pool_t *p, apr_pool_t *ptemp, @@ -796,6 +845,66 @@ static apr_status_t fs_iterate_names(md_store_inspect *inspect, void *baton, md_ return rv; } +static apr_status_t remove_nms_file(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, apr_filetype_e ftype) +{ + inspect_ctx *ctx = baton; + const char *fname; + apr_finfo_t inf; + apr_status_t rv = APR_SUCCESS; + + (void)p; + if (APR_DIR == ftype) goto leave; + if (APR_SUCCESS != (rv = md_util_path_merge(&fname, ptemp, dir, name, NULL))) goto leave; + if (APR_SUCCESS != (rv = apr_stat(&inf, fname, APR_FINFO_MTIME, ptemp))) goto leave; + if (inf.mtime >= ctx->ts) goto leave; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "remove_nms file: %s/%s", dir, name); + rv = apr_file_remove(fname, ptemp); + +leave: + return rv; +} + +static apr_status_t remove_nms_dir(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, apr_filetype_e ftype) +{ + inspect_ctx *ctx = baton; + apr_status_t rv; + const char *fpath; + + (void)ftype; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "remove_nms dir at: %s/%s", dir, name); + if (MD_OK(md_util_path_merge(&fpath, p, dir, name, NULL))) { + ctx->dirname = name; + rv = md_util_files_do(remove_nms_file, ctx, p, fpath, ctx->aspect, NULL); + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } + return rv; +} + +static apr_status_t fs_remove_nms(md_store_t *store, apr_pool_t *p, + apr_time_t modified, md_store_group_t group, + const char *name, const char *aspect) +{ + const char *groupname; + apr_status_t rv; + inspect_ctx ctx; + + ctx.s_fs = FS_STORE(store); + ctx.group = group; + ctx.pattern = name; + ctx.aspect = aspect; + ctx.ts = modified; + groupname = md_store_group_name(group); + + rv = md_util_files_do(remove_nms_dir, &ctx, p, ctx.s_fs->base, groupname, name, NULL); + + return rv; +} + /**************************************************************************************************/ /* moving */ @@ -926,3 +1035,38 @@ static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, md_store_fs_t *s_fs = FS_STORE(store); return md_util_pool_vdo(pfs_move, s_fs, p, from, to, name, archive, NULL); } + +static apr_status_t pfs_rename(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *group_name, *from_dir, *to_dir; + md_store_group_t group; + const char *from, *to; + apr_status_t rv; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + from = va_arg(ap, const char*); + to = va_arg(ap, const char*); + + group_name = md_store_group_name(group); + if ( !MD_OK(md_util_path_merge(&from_dir, ptemp, s_fs->base, group_name, from, NULL)) + || !MD_OK(md_util_path_merge(&to_dir, ptemp, s_fs->base, group_name, to, NULL))) { + goto out; + } + + if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", + from_dir, to_dir); + goto out; + } +out: + return rv; +} + +static apr_status_t fs_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *from, const char *to) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_rename, s_fs, p, group, from, to, NULL); +} diff --git a/modules/md/md_time.c b/modules/md/md_time.c index 4f494b3eff..8076d5be46 100644 --- a/modules/md/md_time.c +++ b/modules/md/md_time.c @@ -44,6 +44,13 @@ int md_timeperiod_has_ended(const md_timeperiod_t *period, apr_time_t time) return (time >= period->start) && (time <= period->end); } +apr_interval_time_t md_timeperiod_remaining(const md_timeperiod_t *period, apr_time_t time) +{ + if (time < period->start) return md_timeperiod_length(period); + if (time < period->end) return period->end - time; + return 0; +} + char *md_timeperiod_print(apr_pool_t *p, const md_timeperiod_t *period) { char tstart[APR_RFC822_DATE_LEN]; @@ -54,31 +61,38 @@ char *md_timeperiod_print(apr_pool_t *p, const md_timeperiod_t *period) return apr_pstrcat(p, tstart, " - ", tend, NULL); } -const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration) +static const char *duration_print(apr_pool_t *p, int roughly, apr_interval_time_t duration) { const char *s = "", *sep = ""; long days = (long)(apr_time_sec(duration) / MD_SECS_PER_DAY); int rem = (int)(apr_time_sec(duration) % MD_SECS_PER_DAY); + s = roughly? "~" : ""; if (days > 0) { - s = apr_psprintf(p, "%ld days", days); - sep = " "; + s = apr_psprintf(p, "%s%ld days", s, days); + if (roughly) return s; + sep = " "; } if (rem > 0) { int hours = (rem / MD_SECS_PER_HOUR); rem = (rem % MD_SECS_PER_HOUR); if (hours > 0) { - s = apr_psprintf(p, "%s%s%02d hours", s, sep, hours); + s = apr_psprintf(p, "%s%s%d hours", s, sep, hours); + if (roughly) return s; sep = " "; } if (rem > 0) { int minutes = (rem / 60); rem = (rem % 60); if (minutes > 0) { - s = apr_psprintf(p, "%s%s%02d minutes", s, sep, minutes); + s = apr_psprintf(p, "%s%s%d minutes", s, sep, minutes); + if (roughly) return s; + sep = " "; } if (rem > 0) { - s = apr_psprintf(p, "%s%s%02d seconds", s, sep, rem); + s = apr_psprintf(p, "%s%s%d seconds", s, sep, rem); + if (roughly) return s; + sep = " "; } } } @@ -91,6 +105,16 @@ const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration) return s; } +const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration) +{ + return duration_print(p, 0, duration); +} + +const char *md_duration_roughly(apr_pool_t *p, apr_interval_time_t duration) +{ + return duration_print(p, 1, duration); +} + static const char *duration_format(apr_pool_t *p, apr_interval_time_t duration) { const char *s = "0"; @@ -127,6 +151,11 @@ static const char *duration_format(apr_pool_t *p, apr_interval_time_t duration) return s; } +const char *md_duration_format(apr_pool_t *p, apr_interval_time_t duration) +{ + return duration_format(p, duration); +} + apr_status_t md_duration_parse(apr_interval_time_t *ptimeout, const char *value, const char *def_unit) { @@ -203,7 +232,7 @@ static apr_status_t percentage_parse(const char *value, int *ppercent) return APR_EINVAL; } -apr_status_t md_timeslice_create(const md_timeslice_t **pts, apr_pool_t *p, +apr_status_t md_timeslice_create(md_timeslice_t **pts, apr_pool_t *p, apr_interval_time_t norm, apr_interval_time_t len) { md_timeslice_t *ts; @@ -215,7 +244,7 @@ apr_status_t md_timeslice_create(const md_timeslice_t **pts, apr_pool_t *p, return APR_SUCCESS; } -const char *md_timeslice_parse(const md_timeslice_t **pts, apr_pool_t *p, +const char *md_timeslice_parse(md_timeslice_t **pts, apr_pool_t *p, const char *val, apr_interval_time_t norm) { md_timeslice_t *ts; diff --git a/modules/md/md_time.h b/modules/md/md_time.h index 80ba9d93db..88e28b20cb 100644 --- a/modules/md/md_time.h +++ b/modules/md/md_time.h @@ -22,16 +22,19 @@ #define MD_SECS_PER_HOUR (60*60) #define MD_SECS_PER_DAY (24*MD_SECS_PER_HOUR) -typedef struct { +typedef struct md_timeperiod_t md_timeperiod_t; + +struct md_timeperiod_t { apr_time_t start; apr_time_t end; -} md_timeperiod_t; +}; apr_time_t md_timeperiod_length(const md_timeperiod_t *period); int md_timeperiod_contains(const md_timeperiod_t *period, apr_time_t time); int md_timeperiod_has_started(const md_timeperiod_t *period, apr_time_t time); int md_timeperiod_has_ended(const md_timeperiod_t *period, apr_time_t time); +apr_interval_time_t md_timeperiod_remaining(const md_timeperiod_t *period, apr_time_t time); char *md_timeperiod_print(apr_pool_t *p, const md_timeperiod_t *period); @@ -39,6 +42,7 @@ char *md_timeperiod_print(apr_pool_t *p, const md_timeperiod_t *period); * Print a human readable form of the give duration in days/hours/min/sec */ const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration); +const char *md_duration_roughly(apr_pool_t *p, apr_interval_time_t duration); /** * Parse a machine readable string duration in the form of NN[unit], where @@ -46,18 +50,19 @@ const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration); */ apr_status_t md_duration_parse(apr_interval_time_t *ptimeout, const char *value, const char *def_unit); +const char *md_duration_format(apr_pool_t *p, apr_interval_time_t duration); typedef struct { apr_interval_time_t norm; /* if > 0, normalized base length */ apr_interval_time_t len; /* length of the timespan */ } md_timeslice_t; -apr_status_t md_timeslice_create(const md_timeslice_t **pts, apr_pool_t *p, +apr_status_t md_timeslice_create(md_timeslice_t **pts, apr_pool_t *p, apr_interval_time_t norm, apr_interval_time_t len); int md_timeslice_eq(const md_timeslice_t *ts1, const md_timeslice_t *ts2); -const char *md_timeslice_parse(const md_timeslice_t **pts, apr_pool_t *p, +const char *md_timeslice_parse(md_timeslice_t **pts, apr_pool_t *p, const char *val, apr_interval_time_t defnorm); const char *md_timeslice_format(const md_timeslice_t *ts, apr_pool_t *p); diff --git a/modules/md/md_util.c b/modules/md/md_util.c index 0b5e192033..6bf65e175d 100644 --- a/modules/md/md_util.c +++ b/modules/md/md_util.c @@ -14,6 +14,7 @@ * limitations under the License. */ +#include <assert.h> #include <stdio.h> #include <apr_lib.h> @@ -70,9 +71,9 @@ apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, . /**************************************************************************************************/ /* data chunks */ -md_data *md_data_create(apr_pool_t *p, const char *data, apr_size_t len) +md_data_t *md_data_create(apr_pool_t *p, const char *data, apr_size_t len) { - md_data *d; + md_data_t *d; d = apr_palloc(p, sizeof(*d)); d->len = len; @@ -80,6 +81,22 @@ md_data *md_data_create(apr_pool_t *p, const char *data, apr_size_t len) return d; } +md_data_t *md_data_make(apr_pool_t *p, apr_size_t len) +{ + md_data_t *d; + + d = apr_palloc(p, sizeof(*d)); + d->len = len; + d->data = apr_pcalloc(p, len); + return d; +} + +void md_data_assign_pcopy(md_data_t *dest, const md_data_t *src, apr_pool_t *p) +{ + dest->data = (src->data && src->len)? apr_pmemdup(p, src->data, src->len) : NULL; + dest->len = dest->data? src->len : 0; +} + static const char * const hex_const[] = { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", @@ -100,7 +117,7 @@ static const char * const hex_const[] = { }; apr_status_t md_data_to_hex(const char **phex, char separator, - apr_pool_t *p, const md_data *data) + apr_pool_t *p, const md_data_t *data) { char *hex, *cp; const char * x; @@ -122,6 +139,47 @@ apr_status_t md_data_to_hex(const char **phex, char separator, } /**************************************************************************************************/ +/* generic arrays */ + +int md_array_remove_at(struct apr_array_header_t *a, int idx) +{ + char *ps, *pe; + + if (idx < 0 || idx >= a->nelts) return 0; + if (idx+1 == a->nelts) { + --a->nelts; + } + else { + ps = (a->elts + (idx * a->elt_size)); + pe = ps + a->elt_size; + memmove(ps, pe, (a->nelts - (idx+1)) * a->elt_size); + --a->nelts; + } + return 1; +} + +int md_array_remove(struct apr_array_header_t *a, void *elem) +{ + int i, n, m; + void **pe; + + assert(sizeof(void*) == a->elt_size); + n = i = 0; + while (i < a->nelts) { + pe = &APR_ARRAY_IDX(a, i, void*); + if (*pe == elem) { + m = a->nelts - (i+1); + if (m > 0) memmove(pe, pe+1, (unsigned)m*sizeof(void*)); + a->nelts--; + n++; + continue; + } + ++i; + } + return n; +} + +/**************************************************************************************************/ /* string related */ int md_array_is_empty(const struct apr_array_header_t *array) @@ -325,7 +383,7 @@ apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool creat: while (i < max && APR_EEXIST == (rv = md_util_fcreatex(&f, tmp, perms, p))) { ++i; - apr_sleep(apr_time_msec(50)); + apr_sleep(apr_time_from_msec(50)); } if (APR_EEXIST == rv && APR_SUCCESS == (rv = apr_file_remove(tmp, p)) @@ -503,7 +561,7 @@ static apr_status_t match_and_do(md_util_fwalk_t *ctx, const char *path, int dep "candidate=%s matches pattern", finfo.name); if (ndepth < ctx->patterns->nelts) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do " - "need to go deepter"); + "need to go deeper"); if (APR_DIR == finfo.filetype) { /* deeper and deeper, irgendwo in der tiefe leuchtet ein licht */ rv = md_util_path_merge(&npath, ptemp, path, finfo.name, NULL); @@ -1023,7 +1081,7 @@ static const unsigned char BASE64URL_CHARS[] = { #define BASE64URL_CHAR(x) BASE64URL_CHARS[ (unsigned int)(x) & 0x3fu ] -apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, +apr_size_t md_util_base64url_decode(md_data_t *decoded, const char *encoded, apr_pool_t *pool) { const unsigned char *e = (const unsigned char *)encoded; @@ -1037,10 +1095,10 @@ apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, } len = (int)(p - e); mlen = (len/4)*4; - *decoded = apr_pcalloc(pool, (apr_size_t)len + 1); + decoded->data = apr_pcalloc(pool, (apr_size_t)len + 1); i = 0; - d = (unsigned char*)*decoded; + d = (unsigned char*)decoded->data; for (; i < mlen; i += 4) { n = ((BASE64URL_UINT6[ e[i+0] ] << 18) + (BASE64URL_UINT6[ e[i+1] ] << 12) + @@ -1069,14 +1127,15 @@ apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, default: /* do nothing */ break; } - return (apr_size_t)(mlen/4*3 + remain); + decoded->len = (apr_size_t)(mlen/4*3 + remain); + return decoded->len; } -const char *md_util_base64url_encode(const char *data, apr_size_t dlen, apr_pool_t *pool) +const char *md_util_base64url_encode(const md_data_t *data, apr_pool_t *pool) { - int i, len = (int)dlen; - apr_size_t slen = ((dlen+2)/3)*4 + 1; /* 0 terminated */ - const unsigned char *udata = (const unsigned char*)data; + int i, len = (int)data->len; + apr_size_t slen = ((data->len+2)/3)*4 + 1; /* 0 terminated */ + const unsigned char *udata = (const unsigned char*)data->data; unsigned char *enc, *p = apr_pcalloc(pool, slen); enc = p; diff --git a/modules/md/md_util.h b/modules/md/md_util.h index bb2667f2d0..0473cdf4cb 100644 --- a/modules/md/md_util.h +++ b/modules/md/md_util.h @@ -35,16 +35,36 @@ apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, . /**************************************************************************************************/ /* data chunks */ -typedef struct md_data md_data; -struct md_data { +typedef struct md_data_t md_data_t; +struct md_data_t { const char *data; apr_size_t len; }; -md_data *md_data_create(apr_pool_t *p, const char *data, apr_size_t len); +#define MD_DATA_CWRAP(d, buffer) md_data_t d = { buffer, sizeof(buffer) } + +md_data_t *md_data_make(apr_pool_t *p, apr_size_t len); +md_data_t *md_data_create(apr_pool_t *p, const char *data, apr_size_t len); + +void md_data_assign_pcopy(md_data_t *dest, const md_data_t *src, apr_pool_t *p); apr_status_t md_data_to_hex(const char **phex, char separator, - apr_pool_t *p, const md_data *data); + apr_pool_t *p, const md_data_t *data); + +/**************************************************************************************************/ +/* generic arrays */ + +/** + * In an array of pointers, remove all entries == elem. Returns the number + * of entries removed. + */ +int md_array_remove(struct apr_array_header_t *a, void *elem); + +/* + * Remove the ith entry from the array. + * @return != 0 iff an entry was removed, e.g. idx was not outside range + */ +int md_array_remove_at(struct apr_array_header_t *a, int idx); /**************************************************************************************************/ /* string related */ @@ -173,9 +193,8 @@ apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms, /**************************************************************************************************/ /* base64 url encodings */ -const char *md_util_base64url_encode(const char *data, - apr_size_t len, apr_pool_t *pool); -apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, +const char *md_util_base64url_encode(const md_data_t *data, apr_pool_t *pool); +apr_size_t md_util_base64url_decode(md_data_t *decoded, const char *encoded, apr_pool_t *pool); /**************************************************************************************************/ diff --git a/modules/md/md_version.h b/modules/md/md_version.h index d7ebed23b6..ebc4375cbf 100644 --- a/modules/md/md_version.h +++ b/modules/md/md_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the md module as c string */ -#define MOD_MD_VERSION "2.0.10" +#define MOD_MD_VERSION "2.2.0" /** * @macro @@ -35,7 +35,7 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define MOD_MD_VERSION_NUM 0x02000a +#define MOD_MD_VERSION_NUM 0x020200 #define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory" diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c index 154a08ef11..538336f0b9 100644 --- a/modules/md/mod_md.c +++ b/modules/md/mod_md.c @@ -37,8 +37,10 @@ #include "md_store.h" #include "md_store_fs.h" #include "md_log.h" +#include "md_ocsp.h" #include "md_result.h" #include "md_reg.h" +#include "md_status.h" #include "md_util.h" #include "md_version.h" #include "md_acme.h" @@ -47,6 +49,7 @@ #include "mod_md.h" #include "mod_md_config.h" #include "mod_md_drive.h" +#include "mod_md_ocsp.h" #include "mod_md_os.h" #include "mod_md_status.h" #include "mod_ssl_openssl.h" @@ -129,7 +132,92 @@ static void init_setups(apr_pool_t *p, server_rec *base_server) } /**************************************************************************************************/ -/* store & registry setup */ +/* notification handling */ + +typedef struct { + const char *reason; /* what the notification is about */ + apr_time_t min_interim; /* minimum time between notifying for this reason */ +} notify_rate; + +static notify_rate notify_rates[] = { + { "renewed", apr_time_from_sec(28 * MD_SECS_PER_DAY) }, /* once per month */ + { "installed", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "expiring", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "errored", apr_time_from_sec(MD_SECS_PER_HOUR) }, /* once per hour */ + { "ocsp-renewed", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "ocsp-errored", apr_time_from_sec(MD_SECS_PER_HOUR) }, /* once per hour */ +}; + +static apr_status_t notify(md_job_t *job, const char *reason, + md_result_t *result, apr_pool_t *p, void *baton) +{ + md_mod_conf_t *mc = baton; + const char * const *argv; + const char *cmdline; + int exit_code; + apr_status_t rv = APR_SUCCESS; + apr_time_t min_interim = 0; + md_timeperiod_t since_last; + const char *log_msg_reason; + int i; + + log_msg_reason = apr_psprintf(p, "message-%s", reason); + for (i = 0; i < (int)(sizeof(notify_rates)/sizeof(notify_rates[0])); ++i) { + if (!strcmp(reason, notify_rates[i].reason)) { + min_interim = notify_rates[i].min_interim; + } + } + if (min_interim > 0) { + since_last.start = md_job_log_get_time_of_latest(job, log_msg_reason); + since_last.end = apr_time_now(); + if (md_timeperiod_length(&since_last) < min_interim) { + /* not enough time has passed since we sent the last notification + * for this reason. */ + return APR_SUCCESS; + } + } + + if (!strcmp("renewed", reason)) { + if (mc->notify_cmd) { + cmdline = apr_psprintf(p, "%s %s", mc->notify_cmd, job->mdomain); + apr_tokenize_to_argv(cmdline, (char***)&argv, p); + rv = md_util_exec(p, argv[0], argv, &exit_code); + + if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL; + if (APR_SUCCESS != rv) { + md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10108)), + "MDNotifyCmd %s failed with exit code %d.", + mc->notify_cmd, exit_code); + md_result_log(result, MD_LOG_ERR); + md_job_log_append(job, "notify-error", result->problem, result->detail); + return rv; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_NOTICE, 0, p, APLOGNO(10059) + "The Managed Domain %s has been setup and changes " + "will be activated on next (graceful) server restart.", job->mdomain); + } + if (mc->message_cmd) { + cmdline = apr_psprintf(p, "%s %s %s", mc->message_cmd, reason, job->mdomain); + apr_tokenize_to_argv(cmdline, (char***)&argv, p); + rv = md_util_exec(p, argv[0], argv, &exit_code); + + if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL; + if (APR_SUCCESS != rv) { + md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10109)), + "MDMessageCmd %s failed with exit code %d.", + mc->message_cmd, exit_code); + md_result_log(result, MD_LOG_ERR); + md_job_log_append(job, "message-error", reason, result->detail); + return rv; + } + } + md_job_log_append(job, log_msg_reason, NULL, NULL); + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* store setup */ static apr_status_t store_file_ev(void *baton, struct md_store_t *store, md_store_fs_ev_t ev, unsigned int group, @@ -150,6 +238,7 @@ static apr_status_t store_file_ev(void *baton, struct md_store_t *store, switch (group) { case MD_SG_CHALLENGES: case MD_SG_STAGING: + case MD_SG_OCSP: rv = md_make_worker_accessible(fname, p); if (APR_ENOTIMPL != rv) { return rv; @@ -185,19 +274,21 @@ static apr_status_t setup_store(md_store_t **pstore, md_mod_conf_t *mc, if (APR_SUCCESS != (rv = md_store_fs_init(pstore, p, base_dir))) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10046)"setup store for %s", base_dir); - goto out; + goto leave; } md_store_fs_set_event_cb(*pstore, store_file_ev, s); if (APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_CHALLENGES, p, s)) || APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_STAGING, p, s)) || APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_ACCOUNTS, p, s)) + || APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_OCSP, p, s)) ) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10047) "setup challenges directory"); + goto leave; } -out: +leave: return rv; } @@ -245,15 +336,20 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) if (md->must_staple < 0) { md->must_staple = md_config_geti(md->sc, MD_CONFIG_MUST_STAPLE); } + if (md->stapling < 0) { + md->stapling = md_config_geti(md->sc, MD_CONFIG_STAPLING); + } } -static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, apr_pool_t *p) +static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, + int *pupdates, apr_pool_t *p) { if (md_contains(md, domain, 0)) { return APR_SUCCESS; } else if (md->transitive) { APR_ARRAY_PUSH(md->domains, const char*) = apr_pstrdup(p, domain); + *pupdates |= MD_UPD_DOMAINS; return APR_SUCCESS; } else { @@ -266,40 +362,27 @@ static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, } } -static apr_status_t md_covers_server(md_t *md, server_rec *s, apr_pool_t *p) +static apr_status_t md_cover_server(md_t *md, server_rec *s, int *pupdates, apr_pool_t *p) { apr_status_t rv; const char *name; int i; - if (APR_SUCCESS == (rv = check_coverage(md, s->server_hostname, s, p)) && s->names) { - for (i = 0; i < s->names->nelts; ++i) { + if (APR_SUCCESS == (rv = check_coverage(md, s->server_hostname, s, pupdates, p))) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "md[%s]: auto add, covers name %s", md->name, s->server_hostname); + for (i = 0; s->names && i < s->names->nelts; ++i) { name = APR_ARRAY_IDX(s->names, i, const char*); - if (APR_SUCCESS != (rv = check_coverage(md, name, s, p))) { + if (APR_SUCCESS != (rv = check_coverage(md, name, s, pupdates, p))) { break; } + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "md[%s]: auto add, covers alias %s", md->name, name); } } return rv; } -static int matches_port_somewhere(server_rec *s, int port) -{ - server_addr_rec *sa; - - for (sa = s->addrs; sa; sa = sa->next) { - if (sa->host_port == port) { - /* host_addr might be general (0.0.0.0) or specific, we count this as match */ - return 1; - } - if (sa->host_port == 0) { - /* wildcard port, answers to all ports. Rare, but may work. */ - return 1; - } - } - return 0; -} - static int uses_port(server_rec *s, int port) { server_addr_rec *sa; @@ -317,61 +400,94 @@ static int uses_port(server_rec *s, int port) return match; } -static apr_status_t detect_supported_ports(md_mod_conf_t *mc, server_rec *s, - apr_pool_t *p, int log_level) +static apr_status_t detect_supported_protocols(md_mod_conf_t *mc, server_rec *s, + apr_pool_t *p, int log_level) { ap_listen_rec *lr; apr_sockaddr_t *sa; + int can_http, can_https; - mc->can_http = 0; - mc->can_https = 0; + if (mc->can_http >= 0 && mc->can_https >= 0) goto set_and_leave; + + can_http = can_https = 0; for (lr = ap_listeners; lr; lr = lr->next) { for (sa = lr->bind_addr; sa; sa = sa->next) { if (sa->port == mc->local_80 && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { - mc->can_http = 1; + can_http = 1; } else if (sa->port == mc->local_443 && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { - mc->can_https = 1; + can_https = 1; } } } - + if (mc->can_http < 0) mc->can_http = can_http; + if (mc->can_https < 0) mc->can_https = can_https; ap_log_error(APLOG_MARK, log_level, 0, s, APLOGNO(10037) - "server seems%s reachable via http: (port 80->%d) " - "and%s reachable via https: (port 443->%d) ", - mc->can_http? "" : " not", mc->local_80, - mc->can_https? "" : " not", mc->local_443); + "server seems%s reachable via http: and%s reachable via https:", + mc->can_http? "" : " not", mc->can_https? "" : " not"); +set_and_leave: return md_reg_set_props(mc->reg, p, mc->can_http, mc->can_https); } -static server_rec *get_https_server(const char *domain, server_rec *base_server) +static server_rec *get_public_https_server(md_t *md, const char *domain, server_rec *base_server) { md_srv_conf_t *sc; md_mod_conf_t *mc; server_rec *s; request_rec r; + int i; sc = md_config_get(base_server); mc = sc->mc; memset(&r, 0, sizeof(r)); - for (s = base_server; s && (mc->local_443 > 0); s = s->next) { - if (!mc->manage_base_server && s == base_server) { - /* we shall not assign ourselves to the base server */ - continue; - } - r.server = s; - if (ap_matches_request_vhost(&r, domain, s->port) && uses_port(s, mc->local_443)) { - return s; + if (!mc->can_https) return NULL; + /* find an ssl server matching domain from MD */ + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->is_ssl || !sc->assigned) continue; + if (base_server == s && !mc->manage_base_server) continue; + if (base_server != s && mc->local_443 > 0 && !uses_port(s, mc->local_443)) continue; + for (i = 0; i < sc->assigned->nelts; ++i) { + if (md == APR_ARRAY_IDX(sc->assigned, i, md_t*)) { + r.server = s; + if (ap_matches_request_vhost(&r, domain, s->port)) { + return s; + } + } } } return NULL; } +static apr_status_t auto_add_domains(md_t *md, server_rec *base_server, apr_pool_t *p) +{ + md_srv_conf_t *sc; + server_rec *s; + apr_status_t rv = APR_SUCCESS; + int updates; + + /* Ad all domain names used in SSL VirtualHosts, if not already there */ + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, base_server, + "md[%s]: auto add domains", md->name); + updates = 0; + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->is_ssl || !sc->assigned || sc->assigned->nelts != 1) continue; + if (md != APR_ARRAY_IDX(sc->assigned, 0, md_t*)) continue; + if (APR_SUCCESS != (rv = md_cover_server(md, s, &updates, p))) { + return rv; + } + } + return rv; +} + static void init_acme_tls_1_domains(md_t *md, server_rec *base_server) { + md_srv_conf_t *sc; + md_mod_conf_t *mc; server_rec *s; int i; const char *domain; @@ -379,10 +495,16 @@ static void init_acme_tls_1_domains(md_t *md, server_rec *base_server) /* Collect those domains that support the "acme-tls/1" protocol. This * is part of the MD (and not tested dynamically), since challenge selection * may be done outside the server, e.g. in the a2md command. */ - apr_array_clear(md->acme_tls_1_domains); + sc = md_config_get(base_server); + mc = sc->mc; + apr_array_clear(md->acme_tls_1_domains); for (i = 0; i < md->domains->nelts; ++i) { domain = APR_ARRAY_IDX(md->domains, i, const char*); - if (NULL == (s = get_https_server(domain, base_server))) { + s = get_public_https_server(md, domain, base_server); + /* If we did not find a specific virtualhost for md and manage + * the base_server, that one is inspected */ + if (NULL == s && mc->manage_base_server) s = base_server; + if (NULL == s) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10168) "%s: no https server_rec found for %s", md->name, domain); continue; @@ -398,15 +520,13 @@ static void init_acme_tls_1_domains(md_t *md, server_rec *base_server) } static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec *base_server, - apr_pool_t *p, apr_pool_t *ptemp) + apr_pool_t *p) { - server_rec *s, *s_https; + server_rec *s; request_rec r; md_srv_conf_t *sc; - apr_status_t rv = APR_SUCCESS; int i; - const char *domain; - apr_array_header_t *servers; + const char *domain, *uri; sc = md_config_get(base_server); @@ -414,8 +534,6 @@ static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec * * is an assigned MD not equal this one, the configuration is in error. */ memset(&r, 0, sizeof(r)); - servers = apr_array_make(ptemp, 5, sizeof(server_rec*)); - for (s = base_server; s; s = s->next) { if (!mc->manage_base_server && s == base_server) { /* we shall not assign ourselves to the base server */ @@ -429,65 +547,15 @@ static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec * if (ap_matches_request_vhost(&r, domain, s->port)) { /* Create a unique md_srv_conf_t record for this server, if there is none yet */ sc = md_config_get_unique(s, p); + if (!sc->assigned) sc->assigned = apr_array_make(p, 2, sizeof(md_t*)); + APR_ARRAY_PUSH(sc->assigned, md_t*) = md; ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10041) - "Server %s:%d matches md %s (config %s)", - s->server_hostname, s->port, md->name, sc->name); - - if (sc->assigned == md) { - /* already matched via another domain name */ - goto next_server; - } - else if (sc->assigned) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10042) - "conflict: MD %s matches server %s, but MD %s also matches.", - md->name, s->server_hostname, sc->assigned->name); - return APR_EINVAL; - } - - /* If this server_rec is only for http: requests. Defined - * alias names do not matter for this MD. - * (see gh issue https://github.com/icing/mod_md/issues/57) - * Otherwise, if server has name or an alias not covered, - * it is by default auto-added (config transitive). - * If mode is "manual", a generated certificate will not match - * all necessary names. */ - if (!mc->local_80 || !uses_port(s, mc->local_80)) { - if (APR_SUCCESS != (rv = md_covers_server(md, s, p))) { - return rv; - } - } - - sc->assigned = md; - APR_ARRAY_PUSH(servers, server_rec*) = s; - - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10043) - "Managed Domain %s applies to vhost %s:%d", md->name, - s->server_hostname, s->port); + "Server %s:%d matches md %s (config %s) for domain %s, " + "has now %d MDs", + s->server_hostname, s->port, md->name, sc->name, + domain, (int)sc->assigned->nelts); - goto next_server; - } - } - next_server: - continue; - } - - if (APR_SUCCESS == rv) { - if (apr_is_empty_array(servers)) { - if (md->renew_mode != MD_RENEW_ALWAYS) { - /* Not an error, but looks suspicious */ - ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10045) - "No VirtualHost matches Managed Domain %s", md->name); - APR_ARRAY_PUSH(mc->unused_names, const char*) = md->name; - } - } - else { - const char *uri; - - /* Found matching server_rec's. Collect all 'ServerAdmin's into MD's contact list */ - apr_array_clear(md->contacts); - for (i = 0; i < servers->nelts; ++i) { - s = APR_ARRAY_IDX(servers, i, server_rec*); if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) { uri = md_util_schemify(p, s->server_admin, "mailto"); if (md_array_str_index(md->contacts, uri, 0, 0) < 0) { @@ -496,50 +564,14 @@ static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec * "%s: added contact %s", md->name, uri); } } + break; } - - if (md->require_https > MD_REQUIRE_OFF) { - /* We require https for this MD, but do we have port 443 (or a mapped one) - * available? */ - if (mc->local_443 <= 0) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10105) - "MDPortMap says there is no port for https (443), " - "but MD %s is configured to require https. This " - "only works when a 443 port is available.", md->name); - return APR_EINVAL; - - } - - /* Ok, we know which local port represents 443, do we have a server_rec - * for MD that has addresses with port 443? */ - s_https = NULL; - for (i = 0; i < servers->nelts; ++i) { - s = APR_ARRAY_IDX(servers, i, server_rec*); - if (matches_port_somewhere(s, mc->local_443)) { - s_https = s; - break; - } - } - - if (!s_https) { - /* Did not find any server_rec that matches this MD *and* has an - * s->addrs match for the https port. Suspicious. */ - ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10106) - "MD %s is configured to require https, but there seems to be " - "no VirtualHost for it that has port %d in its address list. " - "This looks as if it will not work.", - md->name, mc->local_443); - } - } - } - } - return rv; + return APR_SUCCESS; } -static apr_status_t link_mds_to_servers(md_mod_conf_t *mc, server_rec *s, - apr_pool_t *p, apr_pool_t *ptemp) +static apr_status_t link_mds_to_servers(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) { int i; md_t *md; @@ -548,7 +580,7 @@ static apr_status_t link_mds_to_servers(md_mod_conf_t *mc, server_rec *s, apr_array_clear(mc->unused_names); for (i = 0; i < mc->mds->nelts; ++i) { md = APR_ARRAY_IDX(mc->mds, i, md_t*); - if (APR_SUCCESS != (rv = link_md_to_servers(mc, md, s, p, ptemp))) { + if (APR_SUCCESS != (rv = link_md_to_servers(mc, md, s, p))) { goto leave; } } @@ -562,7 +594,7 @@ static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p, md_srv_conf_t *base_conf; md_t *md, *omd; const char *domain; - const md_timeslice_t *ts; + md_timeslice_t *ts; apr_status_t rv = APR_SUCCESS; int i, j; @@ -612,8 +644,6 @@ static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p, return APR_EINVAL; } - init_acme_tls_1_domains(md, base_server); - if (APLOG_IS_LEVEL(base_server, log_level)) { ap_log_error(APLOG_MARK, log_level, 0, base_server, APLOGNO(10039) "Completed MD[%s, CA=%s, Proto=%s, Agreement=%s, renew-mode=%d " @@ -635,7 +665,7 @@ static void load_staged_data(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) for (i = 0; i < mc->mds->nelts; ++i) { md = APR_ARRAY_IDX(mc->mds, i, md_t *); - result = md_result_md_make(p, md); + result = md_result_md_make(p, md->name); if (APR_SUCCESS == (rv = md_reg_load_staging(mc->reg, md, mc->env, result, p))) { ap_log_error( APLOG_MARK, APLOG_INFO, rv, s, APLOGNO(10068) "%s: staged set activated", md->name); @@ -647,39 +677,84 @@ static void load_staged_data(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) } } -static apr_status_t reinit_mds(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +static apr_status_t check_invalid_duplicates(server_rec *base_server) { - md_t *md; - apr_status_t rv = APR_SUCCESS; - int i; + server_rec *s; + md_srv_conf_t *sc; - for (i = 0; i < mc->mds->nelts; ++i) { - md = APR_ARRAY_IDX(mc->mds, i, md_t *); - if (APR_SUCCESS != (rv = md_reg_reinit_state(mc->reg, (md_t*)md, p))) { - ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10172) - "%s: error reinitiazing from store", md->name); - break; + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, base_server, + "cecking duplicate ssl assignments"); + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->assigned) continue; + + if (sc->assigned->nelts > 1 && sc->is_ssl) { + /* duplicate assignment to SSL VirtualHost, not allowed */ + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10042) + "conflict: %d MDs match to SSL VirtualHost %s, there can at most be one.", + (int)sc->assigned->nelts, s->server_hostname); + return APR_EINVAL; + } + } + return APR_SUCCESS; +} + +static apr_status_t check_usage(md_mod_conf_t *mc, md_t *md, server_rec *base_server, + apr_pool_t *p, apr_pool_t *ptemp) +{ + server_rec *s; + md_srv_conf_t *sc; + apr_status_t rv = APR_SUCCESS; + int i, has_ssl; + apr_array_header_t *servers; + + (void)p; + servers = apr_array_make(ptemp, 5, sizeof(server_rec*)); + has_ssl = 0; + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->assigned) continue; + for (i = 0; i < sc->assigned->nelts; ++i) { + if (md == APR_ARRAY_IDX(sc->assigned, i, md_t*)) { + APR_ARRAY_PUSH(servers, server_rec*) = s; + if (sc->is_ssl) has_ssl = 1; + } + } + } + + if (!has_ssl && md->require_https > MD_REQUIRE_OFF) { + /* We require https for this MD, but do we have a SSL vhost? */ + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10105) + "MD %s does not match any VirtualHost with 'SSLEngine on', " + "but is configured to require https. This cannot work.", md->name); + } + if (apr_is_empty_array(servers)) { + if (md->renew_mode != MD_RENEW_ALWAYS) { + /* Not an error, but looks suspicious */ + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10045) + "No VirtualHost matches Managed Domain %s", md->name); + APR_ARRAY_PUSH(mc->unused_names, const char*) = md->name; } } return rv; } -static void init_watched_names(md_mod_conf_t *mc, apr_pool_t *p, apr_pool_t *ptemp, server_rec *s) +static int init_cert_watch_status(md_mod_conf_t *mc, apr_pool_t *p, apr_pool_t *ptemp, server_rec *s) { - const md_t *md; + md_t *md; md_result_t *result; - int i; + int i, count; /* Calculate the list of MD names which we need to watch: * - all MDs that are used somewhere * - all MDs in drive mode 'AUTO' that are not in 'unused_names' */ + count = 0; result = md_result_make(ptemp, APR_SUCCESS); - apr_array_clear(mc->watched_names); for (i = 0; i < mc->mds->nelts; ++i) { - md = APR_ARRAY_IDX(mc->mds, i, const md_t *); + md = APR_ARRAY_IDX(mc->mds, i, md_t*); md_result_set(result, APR_SUCCESS, NULL); - + md->watched = 0; if (md->state == MD_S_ERROR) { md_result_set(result, APR_EGENERAL, "in error state, unable to drive forward. This " @@ -704,8 +779,10 @@ static void init_watched_names(md_mod_conf_t *mc, apr_pool_t *p, apr_pool_t *pte } } - APR_ARRAY_PUSH(mc->watched_names, const char *) = md->name; + md->watched = 1; + ++count; } + return count; } static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog, @@ -755,11 +832,21 @@ static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog, ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10072) "setup md registry"); goto leave; } + md_reg_set_notify_cb(mc->reg, notify, mc); + /* renew on 30% remaining /*/ + rv = md_ocsp_reg_make(&mc->ocsp, p, store, mc->ocsp_renew_window, + AP_SERVER_BASEVERSION, mc->proxy_url); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10196) "setup ocsp registry"); + goto leave; + } + md_ocsp_set_notify_cb(mc->ocsp, notify, mc); + init_ssl(); /* How to bootstrap this module: - * 1. find out if we know where http: and https: requests will arrive + * 1. find out if we know if http: and/or https: requests will arrive * 2. apply the now complete configuration setttings to the MDs * 3. Link MDs to the server_recs they are used in. Detect unused MDs. * 4. Update the store with the MDs. Change domain names, create new MDs, etc. @@ -778,25 +865,63 @@ static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog, * 10. If this list is non-empty, setup a watchdog to run. */ /*1*/ - if (APR_SUCCESS != (rv = detect_supported_ports(mc, s, p, log_level))) goto leave; + if (APR_SUCCESS != (rv = detect_supported_protocols(mc, s, p, log_level))) goto leave; /*2*/ if (APR_SUCCESS != (rv = merge_mds_with_conf(mc, p, s, log_level))) goto leave; /*3*/ - if (APR_SUCCESS != (rv = link_mds_to_servers(mc, s, p, ptemp))) goto leave; + if (APR_SUCCESS != (rv = link_mds_to_servers(mc, s, p))) goto leave; /*4*/ - if (APR_SUCCESS != (rv = md_reg_sync(mc->reg, p, ptemp, mc->mds))) { + if (APR_SUCCESS != (rv = md_reg_sync_start(mc->reg, mc->mds, ptemp))) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10073) "synching %d mds to registry", mc->mds->nelts); goto leave; } /*5*/ load_staged_data(mc, s, p); +leave: + return rv; +} + +static apr_status_t md_post_config_after_ssl(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *s) +{ + md_srv_conf_t *sc; + apr_status_t rv = APR_SUCCESS; + md_mod_conf_t *mc; + int watched, i; + md_t *md; + + (void)ptemp; + (void)plog; + sc = md_config_get(s); + /*6*/ - if (dry_run) goto leave; + if (!sc || !sc->mc || sc->mc->dry_run) goto leave; + mc = sc->mc; + /*7*/ - if (APR_SUCCESS != (rv = reinit_mds(mc, s, p))) goto leave; + if (APR_SUCCESS != (rv = check_invalid_duplicates(s))) { + goto leave; + } + apr_array_clear(mc->unused_names); + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, md_t *); + + if (APR_SUCCESS != (rv = auto_add_domains(md, s, p))) { + goto leave; + } + init_acme_tls_1_domains(md, s); + if (APR_SUCCESS != (rv = check_usage(mc, md, s, p, ptemp))) { + goto leave; + } + if (APR_SUCCESS != (rv = md_reg_sync_finish(mc->reg, md, p, ptemp))) { + ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10172) + "md[%s]: error synching to store", md->name); + goto leave; + } + } /*8*/ - init_watched_names(mc, p, ptemp, s); + watched = init_cert_watch_status(mc, p, ptemp, s); /*9*/ md_reg_cleanup_challenges(mc->reg, p, ptemp, mc->mds); @@ -804,18 +929,23 @@ static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog, * and only staging/challenges may be manipulated */ md_reg_freeze_domains(mc->reg, mc->mds); - if (mc->watched_names->nelts > 0) { + if (watched) { /*10*/ ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10074) - "%d out of %d mds need watching", - mc->watched_names->nelts, mc->mds->nelts); + "%d out of %d mds need watching", watched, mc->mds->nelts); md_http_use_implementation(md_curl_get_impl(p)); - rv = md_start_watching(mc, s, p); + rv = md_renew_start_watching(mc, s, p); } else { - ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075) "no mds to drive"); + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075) "no mds to supervise"); } + + if (!mc->ocsp || md_ocsp_count(mc->ocsp) == 0) goto leave; + + md_http_use_implementation(md_curl_get_impl(p)); + rv = md_ocsp_start_watching(mc, s, p); + leave: return rv; } @@ -877,20 +1007,6 @@ static int md_protocol_switch(conn_rec *c, request_rec *r, server_rec *s, /**************************************************************************************************/ /* Access API to other httpd components */ -static int md_is_managed(server_rec *s) -{ - md_srv_conf_t *conf = md_config_get(s); - - if (conf && conf->assigned) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10076) - "%s: manages server %s", conf->assigned->name, s->server_hostname); - return 1; - } - ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, - "server %s is not managed", s->server_hostname); - return 0; -} - static apr_status_t setup_fallback_cert(md_store_t *store, const md_t *md, server_rec *s, apr_pool_t *p) { @@ -938,23 +1054,27 @@ static apr_status_t get_certificate(server_rec *s, apr_pool_t *p, int fallback, return APR_ENOENT; } + assert(sc->mc); + reg = sc->mc->reg; + assert(reg); + + sc->is_ssl = 1; + if (!sc->assigned) { /* With the new hooks in mod_ssl, we are invoked for all server_rec. It is * therefore normal, when we have nothing to add here. */ return APR_ENOENT; } - - assert(sc->mc); - reg = sc->mc->reg; - assert(reg); - - md = sc->assigned; - if (!md) { - ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10115) - "unable to hand out certificates, as registry can no longer " - "find MD '%s'.", sc->assigned->name); - return APR_ENOENT; + else if (sc->assigned->nelts != 1) { + if (!fallback) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10042) + "conflict: %d MDs match Virtualhost %s which uses SSL, however " + "there can be at most 1.", + (int)sc->assigned->nelts, s->server_hostname); + } + return APR_EINVAL; } + md = APR_ARRAY_IDX(sc->assigned, 0, const md_t*); rv = md_reg_get_cred_files(pkeyfile, pcertfile, reg, MD_SG_DOMAINS, md, p); if (APR_STATUS_IS_ENOENT(rv)) { @@ -990,12 +1110,6 @@ static apr_status_t get_certificate(server_rec *s, apr_pool_t *p, int fallback, return rv; } -static apr_status_t md_get_certificate(server_rec *s, apr_pool_t *p, - const char **pkeyfile, const char **pcertfile) -{ - return get_certificate(s, p, 1, pcertfile, pkeyfile); -} - static int md_add_cert_files(server_rec *s, apr_pool_t *p, apr_array_header_t *cert_files, apr_array_header_t *key_files) @@ -1176,51 +1290,62 @@ static int md_require_https_maybe(request_rec *r) { const md_srv_conf_t *sc; apr_uri_t uri; - const char *s; + const char *s, *host; + const md_t *md; int status; - if (opt_ssl_is_https && r->parsed_uri.path - && strncmp(WELL_KNOWN_PREFIX, r->parsed_uri.path, sizeof(WELL_KNOWN_PREFIX)-1)) { + /* Requests outside the /.well-known path are subject to possible + * https: redirects or HSTS header additions. + */ + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc || !sc->assigned || !sc->assigned->nelts + || !opt_ssl_is_https || !r->parsed_uri.path + || !strncmp(WELL_KNOWN_PREFIX, r->parsed_uri.path, sizeof(WELL_KNOWN_PREFIX)-1)) { + goto declined; + } - sc = ap_get_module_config(r->server->module_config, &md_module); - if (sc && sc->assigned && sc->assigned->require_https > MD_REQUIRE_OFF) { - if (opt_ssl_is_https(r->connection)) { - /* Using https: - * if 'permanent' and no one else set a HSTS header already, do it */ - if (sc->assigned->require_https == MD_REQUIRE_PERMANENT - && sc->mc->hsts_header && !apr_table_get(r->headers_out, MD_HSTS_HEADER)) { - apr_table_setn(r->headers_out, MD_HSTS_HEADER, sc->mc->hsts_header); - } + host = ap_get_server_name_for_url(r); + md = md_get_for_domain(r->server, host); + if (!md) goto declined; + + if (opt_ssl_is_https(r->connection)) { + /* Using https: + * if 'permanent' and no one else set a HSTS header already, do it */ + if (md->require_https == MD_REQUIRE_PERMANENT + && sc->mc->hsts_header && !apr_table_get(r->headers_out, MD_HSTS_HEADER)) { + apr_table_setn(r->headers_out, MD_HSTS_HEADER, sc->mc->hsts_header); + } + } + else { + if (md->require_https > MD_REQUIRE_OFF) { + /* Not using https:, but require it. Redirect. */ + if (r->method_number == M_GET) { + /* safe to use the old-fashioned codes */ + status = ((MD_REQUIRE_PERMANENT == md->require_https)? + HTTP_MOVED_PERMANENTLY : HTTP_MOVED_TEMPORARILY); } else { - /* Not using https:, but require it. Redirect. */ - if (r->method_number == M_GET) { - /* safe to use the old-fashioned codes */ - status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? - HTTP_MOVED_PERMANENTLY : HTTP_MOVED_TEMPORARILY); - } - else { - /* these should keep the method unchanged on retry */ - status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? - HTTP_PERMANENT_REDIRECT : HTTP_TEMPORARY_REDIRECT); - } - - s = ap_construct_url(r->pool, r->uri, r); - if (APR_SUCCESS == apr_uri_parse(r->pool, s, &uri)) { - uri.scheme = (char*)"https"; - uri.port = 443; - uri.port_str = (char*)"443"; - uri.query = r->parsed_uri.query; - uri.fragment = r->parsed_uri.fragment; - s = apr_uri_unparse(r->pool, &uri, APR_URI_UNP_OMITUSERINFO); - if (s && *s) { - apr_table_setn(r->headers_out, "Location", s); - return status; - } + /* these should keep the method unchanged on retry */ + status = ((MD_REQUIRE_PERMANENT == md->require_https)? + HTTP_PERMANENT_REDIRECT : HTTP_TEMPORARY_REDIRECT); + } + + s = ap_construct_url(r->pool, r->uri, r); + if (APR_SUCCESS == apr_uri_parse(r->pool, s, &uri)) { + uri.scheme = (char*)"https"; + uri.port = 443; + uri.port_str = (char*)"443"; + uri.query = r->parsed_uri.query; + uri.fragment = r->parsed_uri.fragment; + s = apr_uri_unparse(r->pool, &uri, APR_URI_UNP_OMITUSERINFO); + if (s && *s) { + apr_table_setn(r->headers_out, "Location", s); + return status; } } } } +declined: return DECLINED; } @@ -1248,6 +1373,7 @@ static void md_hooks(apr_pool_t *pool) * Run again after mod_ssl is done. */ ap_hook_post_config(md_post_config_before_ssl, NULL, mod_ssl, APR_HOOK_MIDDLE); + ap_hook_post_config(md_post_config_after_ssl, mod_ssl, NULL, APR_HOOK_MIDDLE); /* Run once after a child process has been created. */ @@ -1263,22 +1389,18 @@ static void md_hooks(apr_pool_t *pool) /* Status request handlers and contributors */ ap_hook_post_read_request(md_http_cert_status, NULL, mod_ssl, APR_HOOK_MIDDLE); - APR_OPTIONAL_HOOK(ap, status_hook, md_status_hook, NULL, NULL, APR_HOOK_MIDDLE); + APR_OPTIONAL_HOOK(ap, status_hook, md_domains_status_hook, NULL, NULL, APR_HOOK_MIDDLE); + APR_OPTIONAL_HOOK(ap, status_hook, md_ocsp_status_hook, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(md_status_handler, NULL, NULL, APR_HOOK_MIDDLE); -#ifdef SSL_CERT_HOOKS - (void)md_is_managed; - (void)md_get_certificate; + +#ifndef SSL_CERT_HOOKS +#error "This version of mod_md requires Apache httpd 2.4.41 or newer." +#endif APR_OPTIONAL_HOOK(ssl, add_cert_files, md_add_cert_files, NULL, NULL, APR_HOOK_MIDDLE); APR_OPTIONAL_HOOK(ssl, add_fallback_cert_files, md_add_fallback_cert_files, NULL, NULL, APR_HOOK_MIDDLE); APR_OPTIONAL_HOOK(ssl, answer_challenge, md_answer_challenge, NULL, NULL, APR_HOOK_MIDDLE); -#else - (void)md_add_cert_files; - (void)md_add_fallback_cert_files; - (void)md_answer_challenge; - APR_REGISTER_OPTIONAL_FN(md_is_challenge); - APR_REGISTER_OPTIONAL_FN(md_is_managed); - APR_REGISTER_OPTIONAL_FN(md_get_certificate); -#endif + APR_OPTIONAL_HOOK(ssl, init_stapling_status, md_ocsp_init_stapling_status, NULL, NULL, APR_HOOK_MIDDLE); + APR_OPTIONAL_HOOK(ssl, get_stapling_status, md_ocsp_get_stapling_status, NULL, NULL, APR_HOOK_MIDDLE); } diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp index f46e864cff..1bc34b2377 100644 --- a/modules/md/mod_md.dsp +++ b/modules/md/mod_md.dsp @@ -113,6 +113,10 @@ SOURCE=./mod_md_drive.c # End Source File
# Begin Source File
+SOURCE=./mod_md_ocsp.c
+# End Source File
+# Begin Source File
+
SOURCE=./mod_md_os.c
# End Source File
# Begin Source File
@@ -177,6 +181,10 @@ SOURCE=./md_log.c # End Source File
# Begin Source File
+SOURCE=./md_ocsp.c
+# End Source File
+# Begin Source File
+
SOURCE=./md_reg.c
# End Source File
# Begin Source File
diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c index ffb09470db..baa20cc37f 100644 --- a/modules/md/mod_md_config.c +++ b/modules/md/mod_md_config.c @@ -41,25 +41,35 @@ #define MD_DEFAULT_BASE_DIR "md" #endif +static md_timeslice_t def_ocsp_keep_window = { + 0, + MD_TIME_OCSP_KEEP_NORM, +}; + +static md_timeslice_t def_ocsp_renew_window = { + MD_TIME_LIFE_NORM, + MD_TIME_RENEW_WINDOW_DEF, +}; + /* Default settings for the global conf */ static md_mod_conf_t defmc = { NULL, /* list of mds */ #if AP_MODULE_MAGIC_AT_LEAST(20180906, 2) - NULL, /* base dir by default state-dir-relative */ + NULL, /* base dirm by default state-dir-relative */ #else MD_DEFAULT_BASE_DIR, #endif NULL, /* proxy url for outgoing http */ - NULL, /* md_reg */ + NULL, /* md_reg_t */ + NULL, /* md_ocsp_reg_t */ 80, /* local http: port */ 443, /* local https: port */ - 0, /* can http: */ - 0, /* can https: */ + -1, /* can http: */ + -1, /* can https: */ 0, /* manage base server */ MD_HSTS_MAX_AGE_DEFAULT, /* hsts max-age */ NULL, /* hsts headers */ NULL, /* unused names */ - NULL, /* watched names */ NULL, /* init errors hash */ NULL, /* notify cmd */ NULL, /* message cmd */ @@ -67,6 +77,10 @@ static md_mod_conf_t defmc = { 0, /* dry_run flag */ 1, /* server_status_enabled */ 1, /* certificate_status_enabled */ + &def_ocsp_keep_window, /* default time to keep ocsp responses */ + &def_ocsp_renew_window, /* default time to renew ocsp responses */ + "crt.sh", /* default cert checker site name */ + "https://crt.sh?q=", /* default cert checker site url */ }; static md_timeslice_t def_renew_window = { @@ -94,8 +108,11 @@ static md_srv_conf_t defconf = { "ACME", /* ca protocol */ NULL, /* ca agreemnent */ NULL, /* ca challenges array */ + 0, /* stapling */ + 1, /* staple others */ NULL, /* currently defined md */ NULL, /* assigned md, post config */ + 0, /* is_ssl, set during mod_ssl post_config */ }; static md_mod_conf_t *mod_md_config; @@ -118,7 +135,6 @@ static md_mod_conf_t *md_mod_conf_get(apr_pool_t *pool, int create) memcpy(mod_md_config, &defmc, sizeof(*mod_md_config)); mod_md_config->mds = apr_array_make(pool, 5, sizeof(const md_t *)); mod_md_config->unused_names = apr_array_make(pool, 5, sizeof(const md_t *)); - mod_md_config->watched_names = apr_array_make(pool, 5, sizeof(const md_t *)); mod_md_config->env = apr_table_make(pool, 10); mod_md_config->init_errors = apr_hash_make(pool); @@ -143,6 +159,8 @@ static void srv_conf_props_clear(md_srv_conf_t *sc) sc->ca_proto = NULL; sc->ca_agreement = NULL; sc->ca_challenges = NULL; + sc->stapling = DEF_VAL; + sc->staple_others = DEF_VAL; } static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) @@ -158,6 +176,8 @@ static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) to->ca_proto = from->ca_proto; to->ca_agreement = from->ca_agreement; to->ca_challenges = from->ca_challenges; + to->stapling = from->stapling; + to->staple_others = from->staple_others; } static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t *p) @@ -173,6 +193,7 @@ static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t if (from->ca_proto) md->ca_proto = from->ca_proto; if (from->ca_agreement) md->ca_agreement = from->ca_agreement; if (from->ca_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges); + if (from->stapling != DEF_VAL) md->stapling = from->stapling; } void *md_config_create_svr(apr_pool_t *pool, server_rec *s) @@ -198,7 +219,6 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) nsc = (md_srv_conf_t *)apr_pcalloc(pool, sizeof(md_srv_conf_t)); nsc->name = name; nsc->mc = add->mc? add->mc : base->mc; - nsc->assigned = add->assigned? add->assigned : base->assigned; nsc->transitive = (add->transitive != DEF_VAL)? add->transitive : base->transitive; nsc->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https; @@ -213,8 +233,9 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) nsc->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement; nsc->ca_challenges = (add->ca_challenges? apr_array_copy(pool, add->ca_challenges) : (base->ca_challenges? apr_array_copy(pool, base->ca_challenges) : NULL)); + nsc->stapling = (add->stapling != DEF_VAL)? add->stapling : base->stapling; + nsc->staple_others = (add->staple_others != DEF_VAL)? add->staple_others : base->staple_others; nsc->current = NULL; - nsc->assigned = NULL; return nsc; } @@ -487,6 +508,30 @@ static const char *md_config_set_must_staple(cmd_parms *cmd, void *dc, const cha return set_on_off(&config->must_staple, value, cmd->pool); } +static const char *md_config_set_stapling(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + return set_on_off(&config->stapling, value, cmd->pool); +} + +static const char *md_config_set_staple_others(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + return set_on_off(&config->staple_others, value, cmd->pool); +} + static const char *md_config_set_base_server(cmd_parms *cmd, void *dc, const char *value) { md_srv_conf_t *config = md_config_get(cmd->server); @@ -820,6 +865,69 @@ static const char *md_config_set_certificate_status(cmd_parms *cmd, void *dc, co return set_on_off(&sc->mc->certificate_status_enabled, value, cmd->pool); } +static const char *md_config_set_ocsp_keep_window(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + err = md_timeslice_parse(&sc->mc->ocsp_keep_window, cmd->pool, value, MD_TIME_OCSP_KEEP_NORM); + if (err) return apr_psprintf(cmd->pool, "MDStaplingKeepResponse %s", err); + return NULL; +} + +static const char *md_config_set_ocsp_renew_window(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + err = md_timeslice_parse(&sc->mc->ocsp_renew_window, cmd->pool, value, MD_TIME_LIFE_NORM); + if (!err && sc->mc->ocsp_renew_window->norm + && (sc->mc->ocsp_renew_window->len >= sc->mc->ocsp_renew_window->norm)) { + err = "with a length of 100% or more is not allowed."; + } + if (err) return apr_psprintf(cmd->pool, "MDStaplingRenewWindow %s", err); + return NULL; +} + +static const char *md_config_set_cert_check(cmd_parms *cmd, void *dc, + const char *name, const char *url) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + sc->mc->cert_check_name = name; + sc->mc->cert_check_url = url; + return NULL; +} + +static const char *md_config_set_activation_delay(cmd_parms *cmd, void *mconfig, const char *arg) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + apr_interval_time_t delay; + + (void)mconfig; + if (err) { + return err; + } + if (md_duration_parse(&delay, arg, "d") != APR_SUCCESS) { + return "unrecognized duration format"; + } + apr_table_set(sc->mc->env, MD_KEY_ACTIVATION_DELAY, md_duration_format(cmd->pool, delay)); + return NULL; +} const command_rec md_cmds[] = { AP_INIT_TAKE1("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, @@ -860,7 +968,7 @@ const command_rec md_cmds[] = { AP_INIT_TAKE1("MDStoreDir", md_config_set_store_dir, NULL, RSRC_CONF, "the directory for file system storage of managed domain data."), AP_INIT_TAKE1("MDRenewWindow", md_config_set_renew_window, NULL, RSRC_CONF, - "Time length for renewal before certificate expires (defaults to days)"), + "Time length for renewal before certificate expires (defaults to days)."), AP_INIT_TAKE1("MDRequireHttps", md_config_set_require_https, NULL, RSRC_CONF, "Redirect non-secure requests to the https: equivalent."), AP_INIT_RAW_ARGS("MDNotifyCmd", md_config_set_notify_cmd, NULL, RSRC_CONF, @@ -881,6 +989,18 @@ const command_rec md_cmds[] = { "When less time remains for a certificate, send our/log a warning (defaults to days)"), AP_INIT_RAW_ARGS("MDMessageCmd", md_config_set_msg_cmd, NULL, RSRC_CONF, "Set the command run when a message about a domain is issued."), + AP_INIT_TAKE1("MDStapling", md_config_set_stapling, NULL, RSRC_CONF, + "Enable/Disable OCSP Stapling for this/all Managed Domain(s)."), + AP_INIT_TAKE1("MDStapleOthers", md_config_set_staple_others, NULL, RSRC_CONF, + "Enable/Disable OCSP Stapling for certificates not in Managed Domains."), + AP_INIT_TAKE1("MDStaplingKeepResponse", md_config_set_ocsp_keep_window, NULL, RSRC_CONF, + "The amount of time to keep an OCSP response in the store."), + AP_INIT_TAKE1("MDStaplingRenewWindow", md_config_set_ocsp_renew_window, NULL, RSRC_CONF, + "Time length for renewal before OCSP responses expire (defaults to days)."), + AP_INIT_TAKE2("MDCertificateCheck", md_config_set_cert_check, NULL, RSRC_CONF, + "Set name and URL pattern for a certificate monitoring site."), + AP_INIT_TAKE1("MDActivationDelay", md_config_set_activation_delay, NULL, RSRC_CONF, + "How long to delay activation of new certificates"), AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) }; @@ -897,7 +1017,7 @@ apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p) if (mc->hsts_max_age > 0) { mc->hsts_header = apr_psprintf(p, "max-age=%d", mc->hsts_max_age); } - + #if AP_MODULE_MAGIC_AT_LEAST(20180906, 2) if (mc->base_dir == NULL) { mc->base_dir = ap_state_dir_relative(p, MD_DEFAULT_BASE_DIR); @@ -913,6 +1033,7 @@ static md_srv_conf_t *config_get_int(server_rec *s, apr_pool_t *p) ap_assert(sc); if (sc->s != s && p) { sc = md_config_merge(p, &defconf, sc); + sc->s = s; sc->name = apr_pstrcat(p, CONF_S_NAME(s), sc->name, NULL); sc->mc = md_mod_conf_get(p, 1); ap_set_module_config(s->module_config, &md_module, sc); @@ -961,22 +1082,22 @@ int md_config_geti(const md_srv_conf_t *sc, md_config_var_t var) switch (var) { case MD_CONFIG_DRIVE_MODE: return (sc->renew_mode != DEF_VAL)? sc->renew_mode : defconf.renew_mode; - case MD_CONFIG_LOCAL_80: - return sc->mc->local_80; - case MD_CONFIG_LOCAL_443: - return sc->mc->local_443; case MD_CONFIG_TRANSITIVE: return (sc->transitive != DEF_VAL)? sc->transitive : defconf.transitive; case MD_CONFIG_REQUIRE_HTTPS: return (sc->require_https != MD_REQUIRE_UNSET)? sc->require_https : defconf.require_https; case MD_CONFIG_MUST_STAPLE: return (sc->must_staple != DEF_VAL)? sc->must_staple : defconf.must_staple; + case MD_CONFIG_STAPLING: + return (sc->stapling != DEF_VAL)? sc->stapling : defconf.stapling; + case MD_CONFIG_STAPLE_OTHERS: + return (sc->staple_others != DEF_VAL)? sc->staple_others : defconf.staple_others; default: return 0; } } -void md_config_get_timespan(const md_timeslice_t **pspan, const md_srv_conf_t *sc, md_config_var_t var) +void md_config_get_timespan(md_timeslice_t **pspan, const md_srv_conf_t *sc, md_config_var_t var) { switch (var) { case MD_CONFIG_RENEW_WINDOW: @@ -990,3 +1111,19 @@ void md_config_get_timespan(const md_timeslice_t **pspan, const md_srv_conf_t *s } } +const md_t *md_get_for_domain(server_rec *s, const char *domain) +{ + md_srv_conf_t *sc; + const md_t *md; + int i; + + sc = md_config_get(s); + for (i = 0; sc && sc->assigned && i < sc->assigned->nelts; ++i) { + md = APR_ARRAY_IDX(sc->assigned, i, const md_t*); + if (md_contains(md, domain, 0)) goto leave; + } + md = NULL; +leave: + return md; +} + diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h index fde919b2ff..0f1138d9b3 100644 --- a/modules/md/mod_md_config.h +++ b/modules/md/mod_md_config.h @@ -20,6 +20,7 @@ struct apr_hash_t; struct md_store_t; struct md_reg_t; +struct md_ocsp_reg_t; struct md_pkey_spec_t; typedef enum { @@ -28,8 +29,6 @@ typedef enum { MD_CONFIG_BASE_DIR, MD_CONFIG_CA_AGREEMENT, MD_CONFIG_DRIVE_MODE, - MD_CONFIG_LOCAL_80, - MD_CONFIG_LOCAL_443, MD_CONFIG_RENEW_WINDOW, MD_CONFIG_WARN_WINDOW, MD_CONFIG_TRANSITIVE, @@ -38,6 +37,8 @@ typedef enum { MD_CONFIG_MUST_STAPLE, MD_CONFIG_NOTIFY_CMD, MD_CONFIG_MESSGE_CMD, + MD_CONFIG_STAPLING, + MD_CONFIG_STAPLE_OTHERS, } md_config_var_t; typedef struct md_mod_conf_t md_mod_conf_t; @@ -45,7 +46,8 @@ struct md_mod_conf_t { apr_array_header_t *mds; /* all md_t* defined in the config, shared */ const char *base_dir; /* base dir for store */ const char *proxy_url; /* proxy url to use (or NULL) */ - struct md_reg_t *reg; /* md registry instance, singleton, shared */ + struct md_reg_t *reg; /* md registry instance */ + struct md_ocsp_reg_t *ocsp; /* ocsp status registry */ int local_80; /* On which port http:80 arrives */ int local_443; /* On which port https:443 arrives */ @@ -55,7 +57,6 @@ struct md_mod_conf_t { int hsts_max_age; /* max-age of HSTS (rfc6797) header */ const char *hsts_header; /* computed HTST header to use or NULL */ apr_array_header_t *unused_names; /* post config, names of all MDs not assigned to a vhost */ - apr_array_header_t *watched_names; /* post config, names of all MDs that we need to watch */ struct apr_hash_t *init_errors; /* init errors reported with MD name as key */ const char *notify_cmd; /* notification command to execute on signup/renew */ @@ -64,6 +65,10 @@ struct md_mod_conf_t { int dry_run; /* != 0 iff config dry run */ int server_status_enabled; /* if module should add to server-status handler */ int certificate_status_enabled; /* if module should expose /.httpd/certificate-status */ + md_timeslice_t *ocsp_keep_window; /* time that we keep ocsp responses around */ + md_timeslice_t *ocsp_renew_window; /* time before exp. that we start renewing ocsp resp. */ + const char *cert_check_name; /* name of the linked certificate check site */ + const char *cert_check_url; /* url "template for" checking a certificate */ }; typedef struct md_srv_conf_t { @@ -76,16 +81,20 @@ typedef struct md_srv_conf_t { int renew_mode; /* mode of obtaining credentials */ int must_staple; /* certificates should set the OCSP Must Staple extension */ struct md_pkey_spec_t *pkey_spec; /* specification for generating private keys */ - const md_timeslice_t *renew_window; /* time before expiration that starts renewal */ - const md_timeslice_t *warn_window; /* time before expiration that warning are sent out */ + md_timeslice_t *renew_window; /* time before expiration that starts renewal */ + md_timeslice_t *warn_window; /* time before expiration that warning are sent out */ const char *ca_url; /* url of CA certificate service */ const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ const char *ca_agreement; /* accepted agreement uri between CA and user */ struct apr_array_header_t *ca_challenges; /* challenge types configured */ + + int stapling; /* OCSP stapling enabled */ + int staple_others; /* Provide OCSP stapling for non-MD certificates */ md_t *current; /* md currently defined in <MDomainSet xxx> section */ - md_t *assigned; /* post_config: MD that applies to this server or NULL */ + struct apr_array_header_t *assigned; /* post_config: MDs that apply to this server */ + int is_ssl; /* SSLEngine is enabled here */ } md_srv_conf_t; void *md_config_create_svr(apr_pool_t *pool, server_rec *s); @@ -106,7 +115,8 @@ md_srv_conf_t *md_config_get_unique(server_rec *s, apr_pool_t *p); const char *md_config_gets(const md_srv_conf_t *config, md_config_var_t var); int md_config_geti(const md_srv_conf_t *config, md_config_var_t var); -void md_config_get_timespan(const md_timeslice_t **pspan, const md_srv_conf_t *sc, md_config_var_t var); +void md_config_get_timespan(md_timeslice_t **pspan, const md_srv_conf_t *sc, md_config_var_t var); +const md_t *md_get_for_domain(server_rec *s, const char *domain); #endif /* md_config_h */ diff --git a/modules/md/mod_md_drive.c b/modules/md/mod_md_drive.c index 31fdc27225..e412479c6c 100644 --- a/modules/md/mod_md_drive.c +++ b/modules/md/mod_md_drive.c @@ -53,13 +53,13 @@ /**************************************************************************************************/ /* watchdog based impl. */ -#define MD_WATCHDOG_NAME "_md_" +#define MD_RENEW_WATCHDOG_NAME "_md_renew_" static APR_OPTIONAL_FN_TYPE(ap_watchdog_get_instance) *wd_get_instance; static APR_OPTIONAL_FN_TYPE(ap_watchdog_register_callback) *wd_register_callback; static APR_OPTIONAL_FN_TYPE(ap_watchdog_set_callback_interval) *wd_set_interval; -struct md_drive_ctx { +struct md_renew_ctx_t { apr_pool_t *p; server_rec *s; md_mod_conf_t *mc; @@ -68,253 +68,92 @@ struct md_drive_ctx { apr_array_header_t *jobs; }; -typedef struct { - apr_pool_t *p; - md_job_t *job; - md_reg_t *reg; - md_result_t *last; - apr_time_t last_save; -} md_job_result_ctx; - -static void job_result_update(md_result_t *result, void *data) -{ - md_job_result_ctx *ctx = data; - apr_time_t now; - const char *msg, *sep; - - if (md_result_cmp(ctx->last, result)) { - now = apr_time_now(); - md_result_assign(ctx->last, result); - if (result->activity || result->problem || result->detail) { - msg = sep = ""; - if (result->activity) { - msg = apr_psprintf(result->p, "%s", result->activity); - sep = ": "; - } - if (result->detail) { - msg = apr_psprintf(result->p, "%s%s%s", msg, sep, result->detail); - sep = ", "; - } - if (result->problem) { - msg = apr_psprintf(result->p, "%s%sproblem: %s", msg, sep, result->problem); - sep = " "; - } - md_job_log_append(ctx->job, "progress", NULL, msg); - - if (apr_time_msec(now - ctx->last_save) > 500) { - md_job_save(ctx->job, ctx->reg, MD_SG_STAGING, result, ctx->p); - ctx->last_save = now; - } - } - } -} - -static void job_result_observation_start(md_job_t *job, md_result_t *result, - md_reg_t *reg, apr_pool_t *p) -{ - md_job_result_ctx *ctx; - - ctx = apr_pcalloc(p, sizeof(*ctx)); - ctx->p = p; - ctx->job = job; - ctx->reg = reg; - ctx->last = md_result_md_make(p, APR_SUCCESS); - md_result_assign(ctx->last, result); - md_result_on_change(result, job_result_update, ctx); -} - -static void job_result_observation_end(md_job_t *job, md_result_t *result) -{ - (void)job; - md_result_on_change(result, NULL, NULL); -} - -static apr_time_t calc_err_delay(int err_count) -{ - apr_time_t delay = 0; - - if (err_count > 0) { - /* back off duration, depending on the errors we encounter in a row */ - delay = apr_time_from_sec(5 << (err_count - 1)); - if (delay > apr_time_from_sec(60*60)) { - delay = apr_time_from_sec(60*60); - } - } - return delay; -} - -static apr_status_t send_notification(md_drive_ctx *dctx, md_job_t *job, const md_t *md, - const char *reason, md_result_t *result, apr_pool_t *ptemp) -{ - const char * const *argv; - const char *cmdline; - int exit_code; - apr_status_t rv = APR_SUCCESS; - - if (!strcmp("renewed", reason)) { - if (dctx->mc->notify_cmd) { - cmdline = apr_psprintf(ptemp, "%s %s", dctx->mc->notify_cmd, md->name); - apr_tokenize_to_argv(cmdline, (char***)&argv, ptemp); - rv = md_util_exec(ptemp, argv[0], argv, &exit_code); - - if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL; - if (APR_SUCCESS != rv) { - if (!result) result = md_result_make(ptemp, rv); - md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10108)), - "MDNotifyCmd %s failed with exit code %d.", - dctx->mc->notify_cmd, exit_code); - md_result_log(result, MD_LOG_ERR); - md_job_log_append(job, "notify-error", result->problem, result->detail); - goto leave; - } - } - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10059) - "The Managed Domain %s has been setup and changes " - "will be activated on next (graceful) server restart.", md->name); - } - if (dctx->mc->message_cmd) { - cmdline = apr_psprintf(ptemp, "%s %s %s", dctx->mc->message_cmd, reason, md->name); - ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, dctx->s, "Message command: %s", cmdline); - apr_tokenize_to_argv(cmdline, (char***)&argv, ptemp); - rv = md_util_exec(ptemp, argv[0], argv, &exit_code); - - if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL; - if (APR_SUCCESS != rv) { - if (!result) result = md_result_make(ptemp, rv); - md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10109)), - "MDMessageCmd %s failed with exit code %d.", - dctx->mc->notify_cmd, exit_code); - md_result_log(result, MD_LOG_ERR); - md_job_log_append(job, "message-error", reason, result->detail); - goto leave; - } - } -leave: - return rv; -} - -static void check_expiration(md_drive_ctx *dctx, md_job_t *job, const md_t *md, apr_pool_t *ptemp) -{ - md_timeperiod_t since_last; - - ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s, "md(%s): check expiration", md->name); - if (!md_reg_should_warn(dctx->mc->reg, md, dctx->p)) return; - - /* Sends these out at most once per day */ - since_last.start = md_job_log_get_time_of_latest(job, "message-expiring"); - since_last.end = apr_time_now(); - - if (md_timeperiod_length(&since_last) >= apr_time_from_sec(MD_SECS_PER_DAY)) { - ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, dctx->s, - "md(%s): message expiration warning", md->name); - send_notification(dctx, job, md, "expiring", NULL, ptemp); - } -} - -static void process_drive_job(md_drive_ctx *dctx, md_job_t *job, apr_pool_t *ptemp) +static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *ptemp) { const md_t *md; md_result_t *result; - int error_run = 0, fatal_run = 0, save = 0; apr_status_t rv; - md_job_load(job, dctx->mc->reg, MD_SG_STAGING, ptemp); + md_job_load(job); /* Evaluate again on loaded value. Values will change when watchdog switches child process */ if (apr_time_now() < job->next_run) return; - md = md_get_by_name(dctx->mc->mds, job->name); + job->next_run = 0; + if (job->finished && job->notified) { + /* finished and notification handled, nothing to do. */ + goto leave; + } + + md = md_get_by_name(dctx->mc->mds, job->mdomain); AP_DEBUG_ASSERT(md); - result = md_result_md_make(ptemp, md); - if (job->last_result) md_result_assign(result, job->last_result); + result = md_result_md_make(ptemp, md->name); + if (job->last_result) md_result_assign(result, job->last_result); if (md->state == MD_S_MISSING_INFORMATION) { /* Missing information, this will not change until configuration * is changed and server reloaded. */ - fatal_run = 1; + job->fatal_error = 1; + job->next_run = 0; goto leave; } - while (md_will_renew_cert(md)) { - if (job->finished) { - job->next_run = 0; - /* Finished jobs might take a while before the results become valid. - * If that is in the future, request to run then */ - if (apr_time_now() < job->valid_from) { - job->next_run = job->valid_from; - } - else if (md_job_log_get_time_of_latest(job, "notified") == 0) { - rv = send_notification(dctx, job, md, "renewed", result, ptemp); - if (APR_SUCCESS == rv) { - md_job_log_append(job, "notified", NULL, NULL); - save = 1; - } - else { - /* we treat this as an error that triggers retries */ - error_run = 1; - } - } - goto leave; - } - - if (!md_reg_should_renew(dctx->mc->reg, md, dctx->p)) { - ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10053) - "md(%s): no need to renew yet", job->name); - job->next_run = 0; - goto leave; - } - + if (md_will_renew_cert(md)) { /* Renew the MDs credentials in a STAGING area. Might be invoked repeatedly * without discarding previous/intermediate results. * Only returns SUCCESS when the renewal is complete, e.g. STAGING as a * complete set of new credentials. */ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10052) - "md(%s): state=%d, driving", job->name, md->state); - md_job_log_append(job, "renewal-start", NULL, NULL); - /* observe result changes and persist them with limited frequency */ - job_result_observation_start(job, result, dctx->mc->reg, ptemp); - + "md(%s): state=%d, driving", job->mdomain, md->state); + + if (!md_reg_should_renew(dctx->mc->reg, md, dctx->p)) { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10053) + "md(%s): no need to renew", job->mdomain); + goto expiry; + } + + md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg)); md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, result, ptemp); + md_job_end_run(job, result); - job_result_observation_end(job, result); - if (APR_SUCCESS != result->status) { + if (APR_SUCCESS == result->status) { + /* Finished jobs might take a while before the results become valid. + * If that is in the future, request to run then */ + if (apr_time_now() < result->ready_at) { + md_job_retry_at(job, result->ready_at); + goto leave; + } + + if (!job->notified) md_job_notify(job, "renewed", result); + } + else { ap_log_error( APLOG_MARK, APLOG_ERR, result->status, dctx->s, APLOGNO(10056) - "processing %s: %s", job->name, result->detail); - error_run = 1; + "processing %s: %s", job->mdomain, result->detail); md_job_log_append(job, "renewal-error", result->problem, result->detail); - send_notification(dctx, job, md, "errored", result, ptemp); - goto leave; + md_job_holler(job, "errored"); + ap_log_error(APLOG_MARK, APLOG_INFO, 0, dctx->s, APLOGNO(10057) + "%s: encountered error for the %d. time, next run in %s", + job->mdomain, job->error_runs, + md_duration_print(ptemp, job->next_run - apr_time_now())); } - - job->finished = 1; - job->valid_from = result->ready_at; - job->error_runs = 0; - md_job_log_append(job, "renewal-finish", NULL, NULL); - save = 1; } - -leave: - if (!job->finished) { - check_expiration(dctx, job, md, ptemp); - } - - if (fatal_run) { - save = 1; - job->next_run = 0; - } - if (error_run) { - ++job->error_runs; - save = 1; - job->next_run = apr_time_now() + calc_err_delay(job->error_runs); - ap_log_error(APLOG_MARK, APLOG_INFO, 0, dctx->s, APLOGNO(10057) - "%s: encountered error for the %d. time, next run in %s", - job->name, job->error_runs, - md_duration_print(ptemp, job->next_run - apr_time_now())); + +expiry: + if (!job->finished && md_reg_should_warn(dctx->mc->reg, md, dctx->p)) { + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s, + "md(%s): warn about expiration", md->name); + md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg)); + if (APR_SUCCESS == md_job_notify(job, "expiring", result)) { + md_result_set(result, APR_SUCCESS, NULL); + } + md_job_end_run(job, result); } - if (save) { - apr_status_t rv2 = md_job_save(job, dctx->mc->reg, MD_SG_STAGING, result, ptemp); - ap_log_error(APLOG_MARK, APLOG_TRACE1, rv2, dctx->s, "%s: saving job props", job->name); + +leave: + if (job->dirty) { + rv = md_job_save(job, result, ptemp); + ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, dctx->s, "%s: saving job props", job->mdomain); } } @@ -337,7 +176,7 @@ static apr_time_t next_run_default(void) static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) { - md_drive_ctx *dctx = baton; + md_renew_ctx_t *dctx = baton; md_job_t *job; apr_time_t next_run, wait_time; int i; @@ -390,19 +229,18 @@ static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) return APR_SUCCESS; } -apr_status_t md_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +apr_status_t md_renew_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) { apr_allocator_t *allocator; - md_drive_ctx *dctx; + md_renew_ctx_t *dctx; apr_pool_t *dctxp; apr_status_t rv; - const char *name; md_t *md; md_job_t *job; int i; /* We use mod_watchdog to run a single thread in one of the child processes - * to monitor the MDs in mc->watched_names, using the const data in the list + * to monitor the MDs marked as watched, using the const data in the list * mc->mds of our MD structures. * * The data in mc cannot be changed, as we may spawn copies in new child processes @@ -438,29 +276,28 @@ apr_status_t md_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) apr_allocator_max_free_set(allocator, 1); rv = apr_pool_create_ex(&dctxp, p, NULL, allocator); if (rv != APR_SUCCESS) { - ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10062) "md_drive_ctx: create pool"); + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10062) "md_renew_watchdog: create pool"); return rv; } apr_allocator_owner_set(allocator, dctxp); - apr_pool_tag(dctxp, "md_drive_ctx"); + apr_pool_tag(dctxp, "md_renew_watchdog"); dctx = apr_pcalloc(dctxp, sizeof(*dctx)); dctx->p = dctxp; dctx->s = s; dctx->mc = mc; - dctx->jobs = apr_array_make(dctx->p, mc->watched_names->nelts, sizeof(md_job_t *)); - for (i = 0; i < mc->watched_names->nelts; ++i) { - name = APR_ARRAY_IDX(mc->watched_names, i, const char *); - md = md_get_by_name(mc->mds, name); - if (!md) continue; + dctx->jobs = apr_array_make(dctx->p, mc->mds->nelts, sizeof(md_job_t *)); + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, md_t*); + if (!md || !md->watched) continue; - job = md_job_make(p, md->name); + job = md_reg_job_make(mc->reg, md->name, p); APR_ARRAY_PUSH(dctx->jobs, md_job_t*) = job; ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s, - "md(%s): state=%d, created drive job", name, md->state); + "md(%s): state=%d, created drive job", md->name, md->state); - md_job_load(job, mc->reg, MD_SG_STAGING, dctx->p); + md_job_load(job); if (job->error_runs) { /* Server has just restarted. If we encounter an MD job with errors * on a previous driving, we purge its STAGING area. @@ -470,7 +307,7 @@ apr_status_t md_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) */ ap_log_error( APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10064) "md(%s): previous drive job showed %d errors, purging STAGING " - "area to reset.", name, job->error_runs); + "area to reset.", md->name, job->error_runs); md_store_purge(md_reg_store_get(dctx->mc->reg), p, MD_SG_STAGING, md->name); md_store_purge(md_reg_store_get(dctx->mc->reg), p, MD_SG_CHALLENGES, md->name); job->error_runs = 0; @@ -484,13 +321,13 @@ apr_status_t md_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) return APR_SUCCESS; } - if (APR_SUCCESS != (rv = wd_get_instance(&dctx->watchdog, MD_WATCHDOG_NAME, 0, 1, dctx->p))) { + if (APR_SUCCESS != (rv = wd_get_instance(&dctx->watchdog, MD_RENEW_WATCHDOG_NAME, 0, 1, dctx->p))) { ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10066) - "create md watchdog(%s)", MD_WATCHDOG_NAME); + "create md renew watchdog(%s)", MD_RENEW_WATCHDOG_NAME); return rv; } rv = wd_register_callback(dctx->watchdog, 0, dctx, run_watchdog); ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10067) - "register md watchdog(%s)", MD_WATCHDOG_NAME); + "register md renew watchdog(%s)", MD_RENEW_WATCHDOG_NAME); return rv; } diff --git a/modules/md/mod_md_drive.h b/modules/md/mod_md_drive.h index be158674c6..40d6d67653 100644 --- a/modules/md/mod_md_drive.h +++ b/modules/md/mod_md_drive.h @@ -20,14 +20,14 @@ struct md_mod_conf_t; struct md_reg_t; -typedef struct md_drive_ctx md_drive_ctx; +typedef struct md_renew_ctx_t md_renew_ctx_t; int md_will_renew_cert(const md_t *md); /** - * Start driving the certificate procotol for the domains mentioned in mc->watched_names. + * Start driving the certificate renewal for MDs marked with watched. */ -apr_status_t md_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p); +apr_status_t md_renew_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p); diff --git a/modules/md/mod_md_ocsp.c b/modules/md/mod_md_ocsp.c new file mode 100644 index 0000000000..655697140d --- /dev/null +++ b/modules/md/mod_md_ocsp.c @@ -0,0 +1,250 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <assert.h> +#include <apr_optional.h> +#include <apr_time.h> +#include <apr_date.h> +#include <apr_strings.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_log.h> + +#include "mod_watchdog.h" + +#include "md.h" +#include "md_crypt.h" +#include "md_http.h" +#include "md_json.h" +#include "md_ocsp.h" +#include "md_store.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_time.h" +#include "md_util.h" + +#include "mod_md.h" +#include "mod_md_config.h" +#include "mod_md_private.h" +#include "mod_md_ocsp.h" + +static int staple_here(md_srv_conf_t *sc) +{ + if (!sc || !sc->mc->ocsp) return 0; + if (sc->assigned + && sc->assigned->nelts == 1 + && APR_ARRAY_IDX(sc->assigned, 0, const md_t*)->stapling) return 1; + return (md_config_geti(sc, MD_CONFIG_STAPLING) + && md_config_geti(sc, MD_CONFIG_STAPLE_OTHERS)); +} + +apr_status_t md_ocsp_init_stapling_status(server_rec *s, apr_pool_t *p, + X509 *cert, X509 *issuer) +{ + md_srv_conf_t *sc; + const md_t *md; + apr_status_t rv; + + sc = md_config_get(s); + if (!staple_here(sc)) goto declined; + + md = ((sc->assigned || sc->assigned->nelts == 1)? + APR_ARRAY_IDX(sc->assigned, 0, const md_t*) : NULL); + rv = md_ocsp_prime(sc->mc->ocsp, md_cert_wrap(p, cert), + md_cert_wrap(p, issuer), md); + ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, s, "init stapling for: %s", + md? md->name : s->server_hostname); + if (APR_SUCCESS == rv) { + return OK; + } +declined: + return DECLINED; +} + +apr_status_t md_ocsp_get_stapling_status(unsigned char **pder, int *pderlen, + conn_rec *c, server_rec *s, X509 *cert) +{ + md_srv_conf_t *sc; + const md_t *md; + apr_status_t rv; + + sc = md_config_get(s); + if (!staple_here(sc)) goto declined; + + md = ((sc->assigned || sc->assigned->nelts == 1)? + APR_ARRAY_IDX(sc->assigned, 0, const md_t*) : NULL); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "get stapling for: %s", + md? md->name : s->server_hostname); + rv = md_ocsp_get_status(pder, pderlen, sc->mc->ocsp, + md_cert_wrap(c->pool, cert), c->pool, md); + if (APR_STATUS_IS_ENOENT(rv)) goto declined; + return rv; + +declined: + return DECLINED; +} + +/**************************************************************************************************/ +/* watchdog based impl. */ + +#define MD_OCSP_WATCHDOG_NAME "_md_ocsp_" + +static APR_OPTIONAL_FN_TYPE(ap_watchdog_get_instance) *wd_get_instance; +static APR_OPTIONAL_FN_TYPE(ap_watchdog_register_callback) *wd_register_callback; +static APR_OPTIONAL_FN_TYPE(ap_watchdog_set_callback_interval) *wd_set_interval; + +typedef struct md_ocsp_ctx_t md_ocsp_ctx_t; + +struct md_ocsp_ctx_t { + apr_pool_t *p; + server_rec *s; + md_mod_conf_t *mc; + ap_watchdog_t *watchdog; +}; + +static apr_time_t next_run_default(void) +{ + /* we'd like to run at least hourly */ + return apr_time_now() + apr_time_from_sec(MD_SECS_PER_HOUR); +} + +static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) +{ + md_ocsp_ctx_t *octx = baton; + apr_time_t next_run, wait_time; + + /* mod_watchdog invoked us as a single thread inside the whole server (on this machine). + * This might be a repeated run inside the same child (mod_watchdog keeps affinity as + * long as the child lives) or another/new child. + */ + switch (state) { + case AP_WATCHDOG_STATE_STARTING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10197) + "md ocsp watchdog start, ocsp stapling %d certificates", + (int)md_ocsp_count(octx->mc->ocsp)); + break; + + case AP_WATCHDOG_STATE_RUNNING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10198) + "md ocsp watchdog run, ocsp stapling %d certificates", + (int)md_ocsp_count(octx->mc->ocsp)); + + /* Process all drive jobs. They will update their next_run property + * and we schedule ourself at the earliest of all. A job may specify 0 + * as next_run to indicate that it wants to participate in the normal + * regular runs. */ + next_run = next_run_default(); + + md_ocsp_renew(octx->mc->ocsp, octx->p, ptemp, &next_run); + + wait_time = next_run - apr_time_now(); + if (APLOGdebug(octx->s)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10199) + "md ocsp watchdog next run in %s", + md_duration_print(ptemp, wait_time)); + } + wd_set_interval(octx->watchdog, wait_time, octx, run_watchdog); + break; + + case AP_WATCHDOG_STATE_STOPPING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10200) + "md ocsp watchdog stopping"); + break; + } + + return APR_SUCCESS; +} + +static apr_status_t ocsp_remove_old_responses(md_mod_conf_t *mc, apr_pool_t *p) +{ + md_timeperiod_t keep_norm, keep; + + keep_norm.end = apr_time_now(); + keep_norm.start = keep_norm.end - MD_TIME_OCSP_KEEP_NORM; + keep = md_timeperiod_slice_before_end(&keep_norm, mc->ocsp_keep_window); + /* remove any ocsp response older than keep.start */ + return md_ocsp_remove_responses_older_than(mc->ocsp, p, keep.start); +} + +apr_status_t md_ocsp_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +{ + apr_allocator_t *allocator; + md_ocsp_ctx_t *octx; + apr_pool_t *octxp; + apr_status_t rv; + + wd_get_instance = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_get_instance); + wd_register_callback = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_register_callback); + wd_set_interval = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_set_callback_interval); + + if (!wd_get_instance || !wd_register_callback || !wd_set_interval) { + ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, APLOGNO(10201) + "mod_watchdog is required for OCSP stapling"); + return APR_EGENERAL; + } + + /* We want our own pool with own allocator to keep data across watchdog invocations. + * Since we'll run in a single watchdog thread, using our own allocator will prevent + * any confusion in the parent pool. */ + apr_allocator_create(&allocator); + apr_allocator_max_free_set(allocator, 1); + rv = apr_pool_create_ex(&octxp, p, NULL, allocator); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10205) "md_ocsp_watchdog: create pool"); + return rv; + } + apr_allocator_owner_set(allocator, octxp); + apr_pool_tag(octxp, "md_ocsp_watchdog"); + + octx = apr_pcalloc(octxp, sizeof(*octx)); + octx->p = octxp; + octx->s = s; + octx->mc = mc; + + /* Time for some house keeping, before the server goes live (again): + * - we store OCSP responses for each certificate individually by its SHA-1 id + * - this means, as long as certificate do not change, the number of response + * files remains stable. + * - But when a certificate changes (is replaced), the response is obsolete + * - we do not get notified when a certificate is no longer used. An admin + * might just reconfigure or change the content of a file (backup/restore etc.) + * - also, certificates might be added by some openssl config commands or other + * modules that we do not immediately see right at startup. We cannot assume + * that any OCSP response we cannot relate to a certificate RIGHT NOW, is no + * longer needed. + * - since the response files are relatively small, we have no problem with + * keeping them around for a while. We just do not want an ever growing store. + * - The simplest and effective way seems to be to just remove files older + * a certain amount of time. Take a 7 day default and let the admin configure + * it for very special setups. + */ + ocsp_remove_old_responses(mc, octx->p); + + rv = wd_get_instance(&octx->watchdog, MD_OCSP_WATCHDOG_NAME, 0, 1, octx->p); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10202) + "create md ocsp watchdog(%s)", MD_OCSP_WATCHDOG_NAME); + return rv; + } + rv = wd_register_callback(octx->watchdog, 0, octx, run_watchdog); + ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10203) + "register md ocsp watchdog(%s)", MD_OCSP_WATCHDOG_NAME); + return rv; +} + + + diff --git a/modules/md/mod_md_ocsp.h b/modules/md/mod_md_ocsp.h new file mode 100644 index 0000000000..b0894caf11 --- /dev/null +++ b/modules/md/mod_md_ocsp.h @@ -0,0 +1,33 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef mod_md_md_ocsp_h +#define mod_md_md_ocsp_h + + +apr_status_t md_ocsp_init_stapling_status(server_rec *s, apr_pool_t *p, + X509 *cert, X509 *issuer); + +apr_status_t md_ocsp_get_stapling_status(unsigned char **pder, int *pderlen, + conn_rec *c, server_rec *s, X509 *cert); + +/** + * Start watchdog for retrieving/updating ocsp status. + */ +apr_status_t md_ocsp_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p); + + +#endif /* mod_md_md_ocsp_h */ diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c index 9ceadb6007..cde13325c7 100644 --- a/modules/md/mod_md_status.c +++ b/modules/md/mod_md_status.c @@ -32,6 +32,7 @@ #include "md_curl.h" #include "md_crypt.h" #include "md_http.h" +#include "md_ocsp.h" #include "md_json.h" #include "md_status.h" #include "md_store.h" @@ -84,8 +85,9 @@ int md_http_cert_status(request_rec *r) ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "requesting status for MD: %s", md->name); - if (APR_SUCCESS != (rv = md_status_get_md_json(&mdj, md, sc->mc->reg, r->pool))) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10175) + rv = md_status_get_md_json(&mdj, md, sc->mc->reg, sc->mc->ocsp, r->pool); + if (APR_SUCCESS != rv) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10204) "loading md status for %s", md->name); return HTTP_INTERNAL_SERVER_ERROR; } @@ -95,13 +97,13 @@ int md_http_cert_status(request_rec *r) resp = md_json_create(r->pool); - if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_VALID_UNTIL, NULL)) { - md_json_sets(md_json_gets(mdj, MD_KEY_CERT, MD_KEY_VALID_UNTIL, NULL), - resp, MD_KEY_VALID_UNTIL, NULL); + if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_UNTIL, NULL)) { + md_json_sets(md_json_gets(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_UNTIL, NULL), + resp, MD_KEY_VALID, MD_KEY_UNTIL, NULL); } - if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL)) { - md_json_sets(md_json_gets(mdj, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL), - resp, MD_KEY_VALID_FROM, NULL); + if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_FROM, NULL)) { + md_json_sets(md_json_gets(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_FROM, NULL), + resp, MD_KEY_VALID, MD_KEY_FROM, NULL); } if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_SERIAL, NULL)) { md_json_sets(md_json_gets(mdj, MD_KEY_CERT, MD_KEY_SERIAL, NULL), @@ -156,11 +158,15 @@ struct status_info { static void si_val_status(status_ctx *ctx, md_json_t *mdj, const status_info *info) { const char *s = "unknown"; + apr_time_t until; (void)info; - switch (md_json_getl(mdj, MD_KEY_STATE, NULL)) { + switch (md_json_getl(mdj, info->key, NULL)) { case MD_S_INCOMPLETE: s = "incomplete"; break; case MD_S_EXPIRED_DEPRECATED: - case MD_S_COMPLETE: s = "ok"; break; + case MD_S_COMPLETE: + until = md_json_get_time(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_UNTIL, NULL); + s = (!until || until > apr_time_now())? "good" : "expired"; + break; case MD_S_ERROR: s = "error"; break; case MD_S_MISSING_INFORMATION: s = "missing information"; break; default: break; @@ -168,19 +174,36 @@ static void si_val_status(status_ctx *ctx, md_json_t *mdj, const status_info *in apr_brigade_puts(ctx->bb, NULL, NULL, s); } -static void si_val_renew_mode(status_ctx *ctx, md_json_t *mdj, const status_info *info) +static void si_val_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) { - const char *s; - switch (md_json_getl(mdj, info->key, NULL)) { - case MD_RENEW_MANUAL: s = "manual"; break; - case MD_RENEW_ALWAYS: s = "always"; break; - default: s = "auto"; break; + const char *url, *s; + apr_uri_t uri_parsed; + + + s = url = md_json_gets(mdj, info->key, NULL); + if (!url) return; + if (!strcmp(LE_ACMEv2_PROD, url)) { + s = "Let's Encrypt"; } - apr_brigade_puts(ctx->bb, NULL, NULL, s); + else if (!strcmp(LE_ACMEv2_STAGING, url)) { + s = "Let's Encrypt (staging)"; + } + else if (!strcmp(LE_ACMEv1_PROD, url)) { + s = "Let's Encrypt (v1)"; + } + else if (!strcmp(LE_ACMEv1_STAGING, url)) { + s = "Let's Encrypt (v1,staging)"; + } + else if (APR_SUCCESS == apr_uri_parse(ctx->p, url, &uri_parsed)) { + s = uri_parsed.hostname; + + } + apr_brigade_printf(ctx->bb, NULL, NULL, "<a href='%s'>%s</a>", + ap_escape_html2(ctx->p, url, 1), + ap_escape_html2(ctx->p, s, 1)); } - -static void si_val_date(status_ctx *ctx, apr_time_t timestamp) +static void print_date(apr_bucket_brigade *bb, apr_time_t timestamp, const char *title) { if (timestamp > 0) { char ts[128]; @@ -189,160 +212,179 @@ static void si_val_date(status_ctx *ctx, apr_time_t timestamp) apr_size_t len; apr_time_exp_gmt(&texp, timestamp); - apr_strftime(ts, &len, sizeof(ts)-1, "%Y-%m-%dT%H:%M:%SZ", &texp); + apr_strftime(ts, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); ts[len] = '\0'; - apr_strftime(ts2, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); - ts2[len] = '\0'; - apr_brigade_printf(ctx->bb, NULL, NULL, + if (!title) { + apr_strftime(ts2, &len, sizeof(ts)-1, "%Y-%m-%dT%H:%M:%SZ", &texp); + ts2[len] = '\0'; + title = ts2; + } + apr_brigade_printf(bb, NULL, NULL, "<span title='%s' style='white-space: nowrap;'>%s</span>", - ts, ts2); - } - else { - apr_brigade_puts(ctx->bb, NULL, NULL, "-"); + ap_escape_html2(bb->p, title, 1), ts); } } -static void si_val_time(status_ctx *ctx, apr_time_t timestamp) +static void print_time(apr_bucket_brigade *bb, const char *label, apr_time_t t) { - if (timestamp > 0) { - char ts[128]; - char ts2[128]; - apr_time_exp_t texp; - apr_size_t len; - - apr_time_exp_gmt(&texp, timestamp); - apr_strftime(ts, &len, sizeof(ts)-1, "%Y-%m-%dT%H:%M:%SZ", &texp); - ts[len] = '\0'; - apr_strftime(ts2, &len, sizeof(ts2)-1, "%H:%M:%SZ", &texp); + apr_time_t now; + const char *pre, *post, *sep; + char ts[APR_RFC822_DATE_LEN]; + char ts2[128]; + apr_time_exp_t texp; + apr_size_t len; + apr_interval_time_t delta; + + if (t == 0) { + /* timestamp is 0, we use that for "not set" */ + return; + } + apr_time_exp_gmt(&texp, t); + now = apr_time_now(); + pre = post = ""; + sep = (label && strlen(label))? " " : ""; + delta = 0; + apr_rfc822_date(ts, t); + if (t > now) { + delta = t - now; + pre = "in "; + } + else { + delta = now - t; + post = " ago"; + } + if (delta >= (4 * apr_time_from_sec(MD_SECS_PER_DAY))) { + apr_strftime(ts2, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); ts2[len] = '\0'; - apr_brigade_printf(ctx->bb, NULL, NULL, - "<span title='%s' style='white-space: nowrap;'>%s</span>", - ts, ts2); + apr_brigade_printf(bb, NULL, NULL, "%s%s<span title='%s' " + "style='white-space: nowrap;'>%s</span>", + label, sep, ts, ts2); } else { - apr_brigade_puts(ctx->bb, NULL, NULL, "-"); + apr_brigade_printf(bb, NULL, NULL, "%s%s<span title='%s'>%s%s%s</span>", + label, sep, ts, pre, md_duration_roughly(bb->p, delta), post); } } -static void si_val_expires(status_ctx *ctx, md_json_t *mdj, const status_info *info) +static void si_val_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info *info) { - const char *s; - apr_time_t t; + const char *sfrom, *suntil, *sep, *title; + apr_time_t from, until; - (void)info; - s = md_json_dups(ctx->p, mdj, MD_KEY_CERT, MD_KEY_VALID_UNTIL, NULL); - if (s) { - t = apr_date_parse_rfc(s); - si_val_date(ctx, t); + sep = NULL; + sfrom = md_json_gets(mdj, info->key, MD_KEY_FROM, NULL); + from = sfrom? apr_date_parse_rfc(sfrom) : 0; + suntil = md_json_gets(mdj, info->key, MD_KEY_UNTIL, NULL); + until = suntil?apr_date_parse_rfc(suntil) : 0; + + if (from > apr_time_now()) { + apr_brigade_puts(ctx->bb, NULL, NULL, "from "); + print_date(ctx->bb, from, sfrom); + sep = " "; + } + if (until) { + if (sep) apr_brigade_puts(ctx->bb, NULL, NULL, sep); + apr_brigade_puts(ctx->bb, NULL, NULL, "until "); + title = sfrom? apr_psprintf(ctx->p, "%s - %s", sfrom, suntil) : suntil; + print_date(ctx->bb, until, title); } } -static void si_val_valid_from(status_ctx *ctx, md_json_t *mdj, const status_info *info) +static void si_add_header(status_ctx *ctx, const status_info *info) { - const char *s; - apr_time_t t; - - (void)info; - s = md_json_dups(ctx->p, mdj, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL); - if (s) { - t = apr_date_parse_rfc(s); - si_val_date(ctx, t); - } + const char *html = ap_escape_html2(ctx->p, info->label, 1); + apr_brigade_printf(ctx->bb, NULL, NULL, "<th class=\"%s\">%s</th>", html, html); } - -static void si_val_props(status_ctx *ctx, md_json_t *mdj, const status_info *info) -{ - const char *s, *url; - md_pkey_type_t ptype; - int i = 0; - (void)info; - if (md_json_getb(mdj, MD_KEY_MUST_STAPLE, NULL)) { - ++i; - apr_brigade_puts(ctx->bb, NULL, NULL, "must-staple"); - } - s = md_json_gets(mdj, MD_KEY_RENEW_WINDOW, NULL); - if (s) { - if (i++) apr_brigade_puts(ctx->bb, NULL, NULL, " \n"); - apr_brigade_printf(ctx->bb, NULL, NULL, "renew-at[%s]", s); - } - url = s = md_json_gets(mdj, MD_KEY_CA, MD_KEY_URL, NULL); - if (s) { - if (i++) apr_brigade_puts(ctx->bb, NULL, NULL, " \n"); - if (!strcmp(LE_ACMEv2_PROD, s)) s = "letsencrypt(v2)"; - else if (!strcmp(LE_ACMEv1_PROD, s)) s = "letsencrypt(v1)"; - else if (!strcmp(LE_ACMEv2_STAGING, s)) s = "letsencrypt(Testv2)"; - else if (!strcmp(LE_ACMEv1_STAGING, s)) s = "letsencrypt(Testv1)"; - - apr_brigade_printf(ctx->bb, NULL, NULL, "ca=[<a href=\"%s\">%s</a>]", url, s); - } - if (md_json_has_key(mdj, MD_KEY_CONTACTS, NULL)) { - if (i++) apr_brigade_puts(ctx->bb, NULL, NULL, " \n"); - apr_brigade_puts(ctx->bb, NULL, NULL, "contacts=["); - add_json_val(ctx, md_json_getj(mdj, MD_KEY_CONTACTS, NULL)); - apr_brigade_puts(ctx->bb, NULL, NULL, "]"); - } - ptype = md_json_has_key(mdj, MD_KEY_PKEY, MD_KEY_TYPE, NULL)? - (unsigned)md_json_getl(mdj, MD_KEY_PKEY, MD_KEY_TYPE, NULL) : MD_PKEY_TYPE_DEFAULT; - switch (ptype) { - case MD_PKEY_TYPE_RSA: - if (i++) apr_brigade_puts(ctx->bb, NULL, NULL, " \n"); - apr_brigade_printf(ctx->bb, NULL, NULL, "key[RSA(%u)]", - (unsigned)md_json_getl(mdj, MD_KEY_PKEY, MD_PKEY_RSA_BITS_MIN, NULL)); - default: - break; - } +static void si_val_cert_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + md_json_t *jcert; + status_info sub = *info; + + sub.key = MD_KEY_VALID; + jcert = md_json_getj(mdj, info->key, NULL); + if (jcert) si_val_valid_time(ctx, jcert, &sub); } -static void si_val_renewal(status_ctx *ctx, md_json_t *mdj, const status_info *info) +static void si_val_ca_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + md_json_t *jcert; + status_info sub = *info; + + sub.key = MD_KEY_URL; + jcert = md_json_getj(mdj, info->key, NULL); + if (jcert) si_val_url(ctx, jcert, &sub); +} + +static void print_job_summary(apr_bucket_brigade *bb, md_json_t *mdj, const char *key, + const char *separator) { char buffer[HUGE_STRING_LEN]; apr_status_t rv; int finished, errors; apr_time_t t; - const char *s; + const char *s, *line; - (void)info; - if (!md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) { + if (!md_json_has_key(mdj, key, NULL)) { return; } - finished = (int)md_json_getl(mdj, MD_KEY_RENEWAL, MD_KEY_FINISHED, NULL); - errors = (int)md_json_getl(mdj, MD_KEY_RENEWAL, MD_KEY_ERRORS, NULL); - rv = (apr_status_t)md_json_getl(mdj, MD_KEY_RENEWAL, MD_KEY_LAST, MD_KEY_STATUS, NULL); + finished = (int)md_json_getl(mdj, key, MD_KEY_FINISHED, NULL); + errors = (int)md_json_getl(mdj, key, MD_KEY_ERRORS, NULL); + rv = (apr_status_t)md_json_getl(mdj, key, MD_KEY_LAST, MD_KEY_STATUS, NULL); + line = separator? separator : ""; + if (rv != APR_SUCCESS) { - s = md_json_gets(mdj, MD_KEY_RENEWAL, MD_KEY_LAST, MD_KEY_PROBLEM, NULL); - apr_brigade_printf(ctx->bb, NULL, NULL, "Error[%s]: %s", + s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_PROBLEM, NULL); + line = apr_psprintf(bb->p, "%s Error[%s]: %s", line, apr_strerror(rv, buffer, sizeof(buffer)), s? s : ""); } if (finished) { - apr_brigade_puts(ctx->bb, NULL, NULL, "Finished"); - if (md_json_has_key(mdj, MD_KEY_RENEWAL, MD_KEY_VALID_FROM, NULL)) { - s = md_json_gets(mdj, MD_KEY_RENEWAL, MD_KEY_VALID_FROM, NULL); - t = apr_date_parse_rfc(s); - apr_brigade_puts(ctx->bb, NULL, NULL, (apr_time_now() >= t)? - ", valid since: " : ", activate at: "); - si_val_time(ctx, t); - } - apr_brigade_puts(ctx->bb, NULL, NULL, "."); + line = apr_psprintf(bb->p, "%s finished successfully.", line); } - - s = md_json_gets(mdj, MD_KEY_RENEWAL, MD_KEY_LAST, MD_KEY_DETAIL, NULL); - if (s) apr_brigade_puts(ctx->bb, NULL, NULL, s); + else { + s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_DETAIL, NULL); + if (s) line = apr_psprintf(bb->p, "%s %s", line, s); + } errors = (int)md_json_getl(mdj, MD_KEY_ERRORS, NULL); if (errors > 0) { - apr_brigade_printf(ctx->bb, NULL, NULL, ", Had %d errors.", errors); + line = apr_psprintf(bb->p, "%s (%d retr%s) ", line, + errors, (errors > 1)? "y" : "ies"); } - s = md_json_gets(mdj, MD_KEY_RENEWAL, MD_KEY_NEXT_RUN, NULL); - if (s) { - t = apr_date_parse_rfc(s); - apr_brigade_puts(ctx->bb, NULL, NULL, "Next attempt: "); - si_val_time(ctx, t); - apr_brigade_puts(ctx->bb, NULL, NULL, "."); + apr_brigade_puts(bb, NULL, NULL, line); + + t = md_json_get_time(mdj, key, MD_KEY_NEXT_RUN, NULL); + if (t > apr_time_now() && !finished) { + print_time(bb, "\nNext run", t); + } + else if (!strlen(line)) { + apr_brigade_puts(bb, NULL, NULL, "\nOngoing..."); + } +} + +static void si_val_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + apr_time_t t; + + (void)info; + if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) { + print_job_summary(ctx->bb, mdj, MD_KEY_RENEWAL, NULL); + return; + } + + t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); + if (t > apr_time_now()) { + print_time(ctx->bb, "Renew", t); + } + else if (t) { + apr_brigade_puts(ctx->bb, NULL, NULL, "Pending"); + } + else if (MD_RENEW_MANUAL == md_json_getl(mdj, MD_KEY_RENEW_MODE, NULL)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "Manual renew"); } } @@ -351,27 +393,20 @@ static void si_val_remote_check(status_ctx *ctx, md_json_t *mdj, const status_in const char *fingerprint; (void)info; - fingerprint = md_json_gets(mdj, MD_KEY_CERT, MD_KEY_SHA256_FINGERPRINT, NULL); - if (fingerprint) { + if (ctx->mc->cert_check_name && ctx->mc->cert_check_url) { + fingerprint = md_json_gets(mdj, MD_KEY_CERT, MD_KEY_SHA256_FINGERPRINT, NULL); apr_brigade_printf(ctx->bb, NULL, NULL, - "<a href=\"https://censys.io/certificates/%s\">censys.io</a> ", - fingerprint); - apr_brigade_printf(ctx->bb, NULL, NULL, - "<a href=\"https://crt.sh?q=%s\">crt.sh</a> ", fingerprint); + "<a href=\"%s%s\">%s</a> ", + ctx->mc->cert_check_url, fingerprint, ctx->mc->cert_check_name); } } -const status_info status_infos[] = { - { "Name", MD_KEY_NAME, NULL }, - { "Domains", MD_KEY_DOMAINS, NULL }, - { "Status", MD_KEY_STATUS, si_val_status }, - { "Valid", MD_KEY_VALID_FROM, si_val_valid_from }, - { "Expires", MD_KEY_VALID_UNTIL, si_val_expires }, - { "Renew", MD_KEY_RENEW_MODE, si_val_renew_mode }, - { "Check@", MD_KEY_SHA256_FINGERPRINT, si_val_remote_check }, - { "Configuration", MD_KEY_MUST_STAPLE, si_val_props }, - { "Renewal", MD_KEY_NOTIFIED, si_val_renewal }, -}; +static void si_val_stapling(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + (void)info; + if (!md_json_getb(mdj, MD_KEY_STAPLING, NULL)) return; + apr_brigade_puts(ctx->bb, NULL, NULL, "on"); +} static int json_iter_val(void *data, size_t index, md_json_t *json) { @@ -396,6 +431,9 @@ static void add_json_val(status_ctx *ctx, md_json_t *j) else if (md_json_is(MD_JSON_TYPE_OBJECT, j, NULL)) { md_json_writeb(j, MD_JSON_FMT_COMPACT, ctx->bb); } + else if (md_json_is(MD_JSON_TYPE_BOOL, j, NULL)) { + apr_brigade_puts(ctx->bb, NULL, NULL, md_json_getb(j, NULL)? "on" : "off"); + } } static void add_status_cell(status_ctx *ctx, md_json_t *mdj, const status_info *info) @@ -408,6 +446,17 @@ static void add_status_cell(status_ctx *ctx, md_json_t *mdj, const status_info * } } +static const status_info status_infos[] = { + { "Domain", MD_KEY_NAME, NULL }, + { "Names", MD_KEY_DOMAINS, NULL }, + { "Status", MD_KEY_STATE, si_val_status }, + { "Valid", MD_KEY_CERT, si_val_cert_valid_time }, + { "CA", MD_KEY_CA, si_val_ca_url }, + { "Stapling", MD_KEY_STAPLING, si_val_stapling }, + { "Check@", MD_KEY_SHA256_FINGERPRINT, si_val_remote_check }, + { "Activity", MD_KEY_NOTIFIED, si_val_activity }, +}; + static int add_md_row(void *baton, apr_size_t index, md_json_t *mdj) { status_ctx *ctx = baton; @@ -428,7 +477,7 @@ static int md_name_cmp(const void *v1, const void *v2) return strcmp((*(const md_t**)v1)->name, (*(const md_t**)v2)->name); } -int md_status_hook(request_rec *r, int flags) +int md_domains_status_hook(request_rec *r, int flags) { const md_srv_conf_t *sc; const md_mod_conf_t *mc; @@ -437,6 +486,7 @@ int md_status_hook(request_rec *r, int flags) apr_array_header_t *mds; md_json_t *jstatus, *jstock; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for managed domains, start"); sc = ap_get_module_config(r->server->module_config, &md_module); if (!sc) return DECLINED; mc = sc->mc; @@ -452,9 +502,11 @@ int md_status_hook(request_rec *r, int flags) qsort(mds->elts, (size_t)mds->nelts, sizeof(md_t *), md_name_cmp); if (!html) { - apr_brigade_puts(ctx.bb, NULL, NULL, "ManagedDomains: "); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html summary"); + apr_brigade_puts(ctx.bb, NULL, NULL, "Managed Certificates: "); if (mc->mds->nelts > 0) { md_status_take_stock(&jstock, mds, mc->reg, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON summary"); apr_brigade_printf(ctx.bb, NULL, NULL, "total=%d, ok=%d renew=%d errored=%d ready=%d", (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL), (int)md_json_getl(jstock, MD_KEY_COMPLETE, NULL), @@ -468,13 +520,13 @@ int md_status_hook(request_rec *r, int flags) apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); } else if (mc->mds->nelts > 0) { - md_status_get_json(&jstatus, mds, mc->reg, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html table"); + md_status_get_json(&jstatus, mds, mc->reg, mc->ocsp, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON status"); apr_brigade_puts(ctx.bb, NULL, NULL, - "<hr>\n<h2>Managed Domains</h2>\n<table class='md_status'><thead><tr>\n"); + "<hr>\n<h3>Managed Certificates</h3>\n<table class='md_status'><thead><tr>\n"); for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { - apr_brigade_puts(ctx.bb, NULL, NULL, "<th>"); - apr_brigade_puts(ctx.bb, NULL, NULL, status_infos[i].label); - apr_brigade_puts(ctx.bb, NULL, NULL, "</th>"); + si_add_header(&ctx, &status_infos[i]); } apr_brigade_puts(ctx.bb, NULL, NULL, "</tr>\n</thead><tbody>"); md_json_itera(add_md_row, &ctx, jstatus, MD_KEY_MDS, NULL); @@ -483,12 +535,101 @@ int md_status_hook(request_rec *r, int flags) ap_pass_brigade(r->output_filters, ctx.bb); apr_brigade_cleanup(ctx.bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for managed domains, end"); + + return OK; +} + +static void si_val_ocsp_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + apr_time_t t; + + (void)info; + t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); + print_time(ctx->bb, "Refresh", t); + print_job_summary(ctx->bb, mdj, MD_KEY_RENEWAL, ": "); +} + +static const status_info ocsp_status_infos[] = { + { "Domain", MD_KEY_DOMAIN, NULL }, + { "Certificate ID", MD_KEY_ID, NULL }, + { "OCSP Status", MD_KEY_STATUS, NULL }, + { "Stapling Valid", MD_KEY_VALID, si_val_valid_time }, + { "Responder", MD_KEY_URL, si_val_url }, + { "Activity", MD_KEY_NOTIFIED, si_val_ocsp_activity }, +}; + +static int add_ocsp_row(void *baton, apr_size_t index, md_json_t *mdj) +{ + status_ctx *ctx = baton; + int i; + + apr_brigade_printf(ctx->bb, NULL, NULL, "<tr class=\"%s\">", (index % 2)? "odd" : "even"); + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + apr_brigade_puts(ctx->bb, NULL, NULL, "<td>"); + add_status_cell(ctx, mdj, &ocsp_status_infos[i]); + apr_brigade_puts(ctx->bb, NULL, NULL, "</td>"); + } + apr_brigade_puts(ctx->bb, NULL, NULL, "</tr>"); + return 1; +} + +int md_ocsp_status_hook(request_rec *r, int flags) +{ + const md_srv_conf_t *sc; + const md_mod_conf_t *mc; + int i, html; + status_ctx ctx; + md_json_t *jstatus, *jstock; + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for ocsp stapling, start"); + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc) return DECLINED; + mc = sc->mc; + if (!mc || !mc->server_status_enabled) return DECLINED; + + html = !(flags & AP_STATUS_SHORT); + ctx.p = r->pool; + ctx.mc = mc; + ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + ctx.separator = " "; + + if (!html) { + apr_brigade_puts(ctx.bb, NULL, NULL, "Managed Staplings: "); + if (md_ocsp_count(mc->ocsp) > 0) { + md_ocsp_get_summary(&jstock, mc->ocsp, r->pool); + apr_brigade_printf(ctx.bb, NULL, NULL, "total=%d, good=%d revoked=%d unknown=%d", + (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL), + (int)md_json_getl(jstock, MD_KEY_GOOD, NULL), + (int)md_json_getl(jstock, MD_KEY_REVOKED, NULL), + (int)md_json_getl(jstock, MD_KEY_UNKNOWN, NULL)); + } + else { + apr_brigade_puts(ctx.bb, NULL, NULL, "[]"); + } + apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + } + else if (md_ocsp_count(mc->ocsp) > 0) { + md_ocsp_get_status_all(&jstatus, mc->ocsp, r->pool); + apr_brigade_puts(ctx.bb, NULL, NULL, + "<hr>\n<h3>Managed Staplings</h3>\n<table class='md_ocsp_status'><thead><tr>\n"); + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + si_add_header(&ctx, &ocsp_status_infos[i]); + } + apr_brigade_puts(ctx.bb, NULL, NULL, "</tr>\n</thead><tbody>"); + md_json_itera(add_ocsp_row, &ctx, jstatus, MD_KEY_OCSPS, NULL); + apr_brigade_puts(ctx.bb, NULL, NULL, "</td></tr>\n</tbody>\n</table>\n"); + } + + ap_pass_brigade(r->output_filters, ctx.bb); + apr_brigade_cleanup(ctx.bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for ocsp stapling, end"); return OK; } /**************************************************************************************************/ -/* Status handler */ +/* Status handlers */ int md_status_handler(request_rec *r) { @@ -523,12 +664,12 @@ int md_status_handler(request_rec *r) } if (md) { - md_status_get_md_json(&jstatus, md, mc->reg, r->pool); + md_status_get_md_json(&jstatus, md, mc->reg, mc->ocsp, r->pool); } else { mds = apr_array_copy(r->pool, mc->mds); qsort(mds->elts, (size_t)mds->nelts, sizeof(md_t *), md_name_cmp); - md_status_get_json(&jstatus, mds, mc->reg, r->pool); + md_status_get_json(&jstatus, mds, mc->reg, mc->ocsp, r->pool); } if (jstatus) { @@ -542,3 +683,4 @@ int md_status_handler(request_rec *r) } return DECLINED; } + diff --git a/modules/md/mod_md_status.h b/modules/md/mod_md_status.h index 39db4c29ab..f347826cdc 100644 --- a/modules/md/mod_md_status.h +++ b/modules/md/mod_md_status.h @@ -19,7 +19,8 @@ int md_http_cert_status(request_rec *r); -int md_status_hook(request_rec *r, int flags); +int md_domains_status_hook(request_rec *r, int flags); +int md_ocsp_status_hook(request_rec *r, int flags); int md_status_handler(request_rec *r); |