]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Merge of /httpd/httpd/trunk:r1903677
authorStefan Eissing <icing@apache.org>
Thu, 25 Aug 2022 14:11:44 +0000 (14:11 +0000)
committerStefan Eissing <icing@apache.org>
Thu, 25 Aug 2022 14:11:44 +0000 (14:11 +0000)
  *) mod_md: a new directive `MDStoreLocks` can be used on cluster
     setups with a shared file system for `MDStoreDir` to order
     activation of renewed certificates when several cluster nodes are
     restarted at the same time. Store locks are not enabled by default.

     Restored curl_easy cleanup behaviour from v2.4.14 and refactored
     the use of curl_multi for OCSP requests to work with that.
     Fixes <https://github.com/icing/mod_md/issues/293>.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1903678 13f79535-47bb-0310-9956-ffa450edef68

17 files changed:
changes-entries/md_locks_and_fix.txt [new file with mode: 0644]
docs/manual/mod/mod_md.xml
modules/md/md_curl.c
modules/md/md_http.c
modules/md/md_http.h
modules/md/md_log.h
modules/md/md_reg.c
modules/md/md_reg.h
modules/md/md_store.c
modules/md/md_store.h
modules/md/md_store_fs.c
modules/md/md_version.h
modules/md/mod_md.c
modules/md/mod_md_config.c
modules/md/mod_md_config.h
test/modules/md/conftest.py
test/modules/md/test_820_locks.py [new file with mode: 0644]

diff --git a/changes-entries/md_locks_and_fix.txt b/changes-entries/md_locks_and_fix.txt
new file mode 100644 (file)
index 0000000..c70ef1f
--- /dev/null
@@ -0,0 +1,8 @@
+  *) mod_md: a new directive `MDStoreLocks` can be used on cluster
+     setups with a shared file system for `MDStoreDir` to order
+     activation of renewed certificates when several cluster nodes are
+     restarted at the same time. Store locks are not enabled by default.
+
+     Restored curl_easy cleanup behaviour from v2.4.14 and refactored
+     the use of curl_multi for OCSP requests to work with that.
+     Fixes <https://github.com/icing/mod_md/issues/293>.
index 18e2554154451f3c3643e83b0aae3f69d19f7754..d972611768308cc19739bbaebe6e5ed8164cd0bc 100644 (file)
@@ -1405,7 +1405,7 @@ MDMessageCmd /etc/apache/md-message
         </usage>
     </directivesynopsis>
 
-        <directivesynopsis>
+    <directivesynopsis>
         <name>MDRetryFailover</name>
         <description></description>
         <syntax>MDRetryFailover <var>number</var></syntax>
@@ -1424,4 +1424,38 @@ MDMessageCmd /etc/apache/md-message
         </usage>
     </directivesynopsis>
 
+    <directivesynopsis>
+        <name>MDStoreLocks</name>
+        <description></description>
+        <syntax>MDStoreLocks on|off|<var>duration</var></syntax>
+        <default>MDStoreLocks off</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <compatibility>Available in version 2.4.55 and later</compatibility>
+        <usage>
+            <p>
+                Enable this to use a lock file on server startup when
+                <directive>MDStoreDir</directive> is synchronized with the server
+                configuration and renewed certificates are activated.
+            </p><p>
+                Locking is intended for setups in a cluster that have a shared
+                file system for MDStoreDir. It will protect the activation of
+                renewed certificates when cluster nodes are restarted/reloaded
+                at the same time. Under the condition that the shared file
+                 system does support file locking.
+            </p><p>
+                The default duration to obtain the lock is 5 seconds. If the log
+                cannot be obtained, an error is logged and the server startup will
+                continue. This may result in a cluster node to still use the
+                previous certificate afterwards.
+            </p><p>
+                A higher timeout will reduce that likelihood, but may delay server
+                startups/reloads in case the locks are not properly handled in
+                the underlying file system. A lock should only be held by a
+                httpd instance for a short duration.
+            </p>
+        </usage>
+    </directivesynopsis>
+
 </modulesynopsis>
index 0a399f9a50a52e5aa797ba5827de97949581bbb8..217e8579dd834dbd1d0683b6d853146a818b1280 100644 (file)
@@ -253,18 +253,17 @@ static apr_status_t internals_setup(md_http_request_t *req)
             rv = APR_EGENERAL;
             goto leave;
         }
+        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);
     }
     else {
         md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "reusing curl instance from http");
     }
 
-    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 = apr_pcalloc(req->pool, sizeof(*internals));
     internals->curl = curl;
         
@@ -442,33 +441,41 @@ 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);
+    assert(curlm);
+    assert(internals);
+    if (internals->curlm == NULL) {
         internals->curlm = curlm;
     }
+    assert(internals->curlm == curlm);
+    curl_multi_add_handle(curlm, internals->curl);
 }
 
-static void remove_from_curlm(md_http_request_t *req, CURLM *curlm)
+static void remove_from_curlm_and_destroy(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;
-    }
+    assert(curlm);
+    assert(internals);
+    assert(internals->curlm == curlm);
+    curl_multi_remove_handle(curlm, internals->curl);
+    internals->curlm = NULL;
+    md_http_req_destroy(req);
 }
     
 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_t *sub_http;
     md_http_request_t *req;
     CURLM *curlm = NULL;
     CURLMcode mc;
     struct CURLMsg *curlmsg;
+    apr_array_header_t *http_spares;
     apr_array_header_t *requests;
     int i, running, numfds, slowdown, msgcount;
     apr_status_t rv;
     
+    http_spares = apr_array_make(p, 10, sizeof(md_http_t*));
     requests = apr_array_make(p, 10, sizeof(md_http_request_t*));
     curlm = curl_multi_init();
     if (!curlm) {
@@ -481,35 +488,46 @@ static apr_status_t md_curl_multi_perform(md_http_t *http, apr_pool_t *p,
     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, 
+            if (http_spares->nelts > 0) {
+                sub_http = *(md_http_t **)(apr_array_pop(http_spares));
+            }
+            else {
+                rv = md_http_clone(&sub_http, p, http);
+                if (APR_SUCCESS != rv) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p,
                                   "multi_perform[%d reqs]: setup failed", requests->nelts);
+                    goto leave;
                 }
-                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, 
+
+            rv = nextreq(&req, baton, sub_http, requests->nelts);
+            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 (!requests->nelts) {
                     goto leave;
                 }
                 break;
             }
-            else {
-                md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, 
+            else if (APR_SUCCESS != rv) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p,
                               "multi_perform[%d reqs]: nextreq() failed", requests->nelts);
+                APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http;
                 goto leave;
             }
+
+            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);
+                APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http;
+                goto leave;
+            }
+
+            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);
         }
     
         mc = curl_multi_perform(curlm, &running);
@@ -544,9 +562,10 @@ static apr_status_t md_curl_multi_perform(md_http_t *http, apr_pool_t *p,
                                   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);
+                    sub_http = req->http;
+                    APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http;
+                    remove_from_curlm_and_destroy(req, curlm);
                 }
                 else {
                     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, 
@@ -563,8 +582,9 @@ leave:
     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);
+        sub_http = req->http;
+        APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http;
+        remove_from_curlm_and_destroy(req, curlm);
     }
     if (curlm) curl_multi_cleanup(curlm);
     return rv;
@@ -585,7 +605,19 @@ static void md_curl_req_cleanup(md_http_request_t *req)
     md_curl_internals_t *internals = req->internals;
     if (internals) {
         if (internals->curl) {
-            curl_easy_cleanup(internals->curl);
+            CURL *curl = md_http_get_impl_data(req->http);
+            if (curl == internals->curl) {
+                /* NOP: we have this curl at the md_http_t already */
+            }
+            else if (!curl) {
+                /* no curl at the md_http_t yet, install this one */
+                md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "register curl instance at http");
+                md_http_set_impl_data(req->http, internals->curl);
+            }
+            else {
+                /* There already is a curl at the md_http_t and it's not this one. */
+                curl_easy_cleanup(internals->curl);
+            }
         }
         if (internals->req_hdrs) curl_slist_free_all(internals->req_hdrs);
         req->internals = NULL;
index 74db961301eb3503b9086f15e6bc912ed3fd35c8..0d21e7b14c6d3cddd03b793370403baa9bb9fbc1 100644 (file)
@@ -92,6 +92,25 @@ apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_a
     return APR_SUCCESS;
 }
 
+apr_status_t md_http_clone(md_http_t **phttp,
+                           apr_pool_t *p, md_http_t *source_http)
+{
+    apr_status_t rv;
+
+    rv = md_http_create(phttp, p, source_http->user_agent, source_http->proxy_url);
+    if (APR_SUCCESS == rv) {
+        (*phttp)->resp_limit = source_http->resp_limit;
+        (*phttp)->timeout = source_http->timeout;
+        if (source_http->unix_socket_path) {
+            (*phttp)->unix_socket_path = apr_pstrdup(p, source_http->unix_socket_path);
+        }
+        if (source_http->ca_file) {
+            (*phttp)->ca_file = apr_pstrdup(p, source_http->ca_file);
+        }
+    }
+    return rv;
+}
+
 void md_http_set_impl_data(md_http_t *http, void *data)
 {
     http->impl_data = data;
@@ -183,7 +202,6 @@ static apr_status_t req_set_body_data(md_http_request_t *req, const char *conten
         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;
         }
     }
@@ -315,10 +333,16 @@ apr_status_t md_http_POSTd_create(md_http_request_t **preq, md_http_t *http, con
     apr_status_t rv;
     
     rv = req_create(&req, http, "POST", url, headers);
+    if (APR_SUCCESS != rv) goto cleanup;
+    rv = req_set_body_data(req, content_type, body);
+cleanup:
     if (APR_SUCCESS == rv) {
-        rv = req_set_body_data(req, content_type, body);
+        *preq = req;
+    }
+    else {
+        *preq = NULL;
+        if (req) md_http_req_destroy(req);
     }
-    *preq = (APR_SUCCESS == rv)? req : NULL;
     return rv;
 }
 
index c210aa99130f0a51da095ba7179b9d26c5d1739b..2f250f6d769e3a195aebb1d9937640ea1efd5695 100644 (file)
@@ -87,6 +87,13 @@ 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);
 
+/**
+ * Clone a http instance, inheriting all settings from source_http.
+ * The cloned instance is not tied in any way to the source.
+ */
+apr_status_t md_http_clone(md_http_t **phttp,
+                           apr_pool_t *p, md_http_t *source_http);
+
 /**
  * Set the timeout for the complete request. This needs to take everything from
  * DNS looksups, to conntects, to transfer of all data into account and should
index 73885f268e197c8e6cf6be4218360130e4a4b9b7..19e688f7fd752ecd1c9d61cae51c59b0acc703f7 100644 (file)
@@ -38,6 +38,10 @@ typedef enum {
 
 #define MD_LOG_MARK     __FILE__,__LINE__
 
+#ifndef APLOGNO
+#define APLOGNO(n)              "AH" #n ": "
+#endif
+
 const char *md_log_level_name(md_log_level_t level);
 
 int md_log_is_level(apr_pool_t *p, md_log_level_t level);
index 21374fc1af95fa5d8af51c1726e2a653f6d17f93..8bceb0eb47086f06e388d7ea13e75067a2aa61a4 100644 (file)
@@ -55,6 +55,8 @@ struct md_reg_t {
     void *notify_ctx;
     apr_time_t min_delay;
     int retry_failover;
+    int use_store_locks;
+    apr_time_t lock_wait_timeout;
 };
 
 /**************************************************************************************************/
@@ -83,7 +85,8 @@ static apr_status_t load_props(md_reg_t *reg, apr_pool_t *p)
 
 apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *store,
                            const char *proxy_url, const char *ca_file,
-                           apr_time_t min_delay, int retry_failover)
+                           apr_time_t min_delay, int retry_failover,
+                           int use_store_locks, apr_time_t lock_wait_timeout)
 {
     md_reg_t *reg;
     apr_status_t rv;
@@ -100,6 +103,8 @@ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *st
                     apr_pstrdup(p, ca_file) : NULL;
     reg->min_delay = min_delay;
     reg->retry_failover = retry_failover;
+    reg->use_store_locks = use_store_locks;
+    reg->lock_wait_timeout = lock_wait_timeout;
 
     md_timeslice_create(&reg->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF); 
     md_timeslice_create(&reg->warn_window, p, MD_TIME_LIFE_NORM, MD_TIME_WARN_WINDOW_DEF); 
@@ -1235,6 +1240,52 @@ apr_status_t md_reg_load_staging(md_reg_t *reg, const md_t *md, apr_table_t *env
     return md_util_pool_vdo(run_load_staging, reg, p, md, env, result, NULL);
 }
 
+apr_status_t md_reg_load_stagings(md_reg_t *reg, apr_array_header_t *mds,
+                                  apr_table_t *env, apr_pool_t *p)
+{
+    apr_status_t rv = APR_SUCCESS;
+    md_t *md;
+    md_result_t *result;
+    int i;
+
+    for (i = 0; i < mds->nelts; ++i) {
+        md = APR_ARRAY_IDX(mds, i, md_t *);
+        result = md_result_md_make(p, md->name);
+        rv = md_reg_load_staging(reg, md, env, result, p);
+        if (APR_SUCCESS == rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, APLOGNO(10068)
+                          "%s: staged set activated", md->name);
+        }
+        else if (!APR_STATUS_IS_ENOENT(rv)) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, APLOGNO(10069)
+                          "%s: error loading staged set", md->name);
+        }
+    }
+
+    return rv;
+}
+
+apr_status_t md_reg_lock_global(md_reg_t *reg, apr_pool_t *p)
+{
+    apr_status_t rv = APR_SUCCESS;
+
+    if (reg->use_store_locks) {
+        rv = md_store_lock_global(reg->store, p, reg->lock_wait_timeout);
+        if (APR_SUCCESS != rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+                          "unable to acquire global store lock");
+        }
+    }
+    return rv;
+}
+
+void md_reg_unlock_global(md_reg_t *reg, apr_pool_t *p)
+{
+    if (reg->use_store_locks) {
+        md_store_unlock_global(reg->store, p);
+    }
+}
+
 apr_status_t md_reg_freeze_domains(md_reg_t *reg, apr_array_header_t *mds)
 {
     apr_status_t rv = APR_SUCCESS;
index ccaf10253ad18af9e3ee86cfe671dca84b87911f..58ee16ac62f9452c933e5f8fcd980ed65da75497 100644 (file)
@@ -34,10 +34,18 @@ typedef struct md_reg_t md_reg_t;
 
 /**
  * Create the MD registry, using the pool and store.
+ * @param preg on APR_SUCCESS, the create md_reg_t
+ * @param pm memory pool to use for creation
+ * @param store the store to base on
+ * @param proxy_url optional URL of a proxy to use for requests
+ * @param ca_file  optioinal CA trust anchor file to use
+ * @param min_delay minimum delay between renewal attempts for a domain
+ * @param retry_failover numer of failed renewals attempt to fail over to alternate ACME ca
  */
 apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *pm, md_store_t *store,
                            const char *proxy_url, const char *ca_file,
-                           apr_time_t min_delay, int retry_failover);
+                           apr_time_t min_delay, int retry_failover,
+                           int use_store_locks, apr_time_t lock_wait_timeout);
 
 md_store_t *md_reg_store_get(md_reg_t *reg);
 
@@ -270,9 +278,36 @@ 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);
 
+/**
+ * Check given MDomains for new data in staging areas and, if it exists, load
+ * the new credentials. On encountering errors, leave the credentails as
+ * they are.
+ */
+apr_status_t md_reg_load_stagings(md_reg_t *reg, apr_array_header_t *mds,
+                                  apr_table_t *env, apr_pool_t *p);
+
 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);
 
 struct md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p);
 
+/**
+ * Acquire a cooperative, global lock on registry modifications. Will
+ * do nothing if locking is not configured.
+ *
+ * This will only prevent other children/processes/cluster nodes from
+ * doing the same and does not protect individual store functions from
+ * being called without it.
+ * @param reg the registy
+ * @param p memory pool to use
+ * @param max_wait maximum time to wait in order to acquire
+ * @return APR_SUCCESS when lock was obtained
+ */
+apr_status_t md_reg_lock_global(md_reg_t *reg, apr_pool_t *p);
+
+/**
+ * Realease the global registry lock. Will do nothing if there is no lock.
+ */
+void md_reg_unlock_global(md_reg_t *reg, apr_pool_t *p);
+
 #endif /* mod_md_md_reg_h */
index 29f3632d92242ccc2d801171e6f6484fc9a13551..59dbd676e9c618e9b336b373731866ad7965164c 100644 (file)
@@ -374,3 +374,12 @@ apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_stor
     return md_store_iter(insp_md, &ctx, store, p, group, pattern, MD_FN_MD, MD_SV_JSON);
 }
 
+apr_status_t md_store_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait)
+{
+    return store->lock_global(store, p, max_wait);
+}
+
+void md_store_unlock_global(md_store_t *store, apr_pool_t *p)
+{
+    store->unlock_global(store, p);
+}
index e252c27909917231208d779806dcf5d5cd785cad..73c840fc57f6205a693feb2b10898bb580744f99 100644 (file)
@@ -204,7 +204,23 @@ apr_status_t md_store_iter_names(md_store_inspect *inspect, void *baton, md_stor
 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);
 
+/**
+ * Acquire a cooperative, global lock on store modifications.
+
+ * This will only prevent other children/processes/cluster nodes from
+ * doing the same and does not protect individual store functions from
+ * being called without it.
+ * @param store the store
+ * @param p memory pool to use
+ * @param max_wait maximum time to wait in order to acquire
+ * @return APR_SUCCESS when lock was obtained
+ */
+apr_status_t md_store_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait);
 
+/**
+ * Realease the global store lock. Will do nothing if there is no lock.
+ */
+void md_store_unlock_global(md_store_t *store, apr_pool_t *p);
 
 /**************************************************************************************************/
 /* Storage handling utils */
@@ -303,6 +319,8 @@ typedef apr_time_t md_store_get_modified_cb(md_store_t *store, md_store_group_t
 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);
+typedef apr_status_t md_store_lock_global_cb(md_store_t *store, apr_pool_t *p, apr_time_t max_wait);
+typedef void md_store_unlock_global_cb(md_store_t *store, apr_pool_t *p);
 
 struct md_store_t {
     md_store_save_cb *save;
@@ -317,6 +335,8 @@ struct md_store_t {
     md_store_is_newer_cb *is_newer;
     md_store_get_modified_cb *get_modified;
     md_store_remove_nms_cb *remove_nms;
+    md_store_lock_global_cb *lock_global;
+    md_store_unlock_global_cb *unlock_global;
 };
 
 
index c2a4a4e49300bdcf1f6e67bf5863a905f138db1c..35c24b4180435a2b021894fd7e49fc5f1641ee48 100644 (file)
@@ -39,6 +39,7 @@
 /* file system based implementation of md_store_t */
 
 #define MD_STORE_VERSION        3
+#define MD_FS_LOCK_NAME         "store.lock"
 
 typedef struct {
     apr_fileperms_t dir;
@@ -60,6 +61,8 @@ struct md_store_fs_t {
     
     int port_80;
     int port_443;
+
+    apr_file_t *global_lock;
 };
 
 #define FS_STORE(store)     (md_store_fs_t*)(((char*)store)-offsetof(md_store_fs_t, s))
@@ -101,6 +104,9 @@ static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_grou
 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 fs_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait);
+static void fs_unlock_global(md_store_t *store, 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)
 {
@@ -296,7 +302,9 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa
     s_fs->s.is_newer = fs_is_newer;
     s_fs->s.get_modified = fs_get_modified;
     s_fs->s.remove_nms = fs_remove_nms;
-    
+    s_fs->s.lock_global = fs_lock_global;
+    s_fs->s.unlock_global = fs_unlock_global;
+
     /* by default, everything is only readable by the current user */ 
     s_fs->def_perms.dir = MD_FPROT_D_UONLY;
     s_fs->def_perms.file = MD_FPROT_F_UONLY;
@@ -1095,3 +1103,67 @@ static apr_status_t fs_rename(md_store_t *store, apr_pool_t *p,
     md_store_fs_t *s_fs = FS_STORE(store);
     return md_util_pool_vdo(pfs_rename, s_fs, p, group, from, to, NULL);
 }
+
+static apr_status_t fs_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    apr_status_t rv;
+    const char *lpath;
+    apr_time_t end;
+
+    if (s_fs->global_lock) {
+        rv = APR_EEXIST;
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "already locked globally");
+        goto cleanup;
+    }
+
+    rv = md_util_path_merge(&lpath, p, s_fs->base, MD_FS_LOCK_NAME, NULL);
+    if (APR_SUCCESS != rv) goto cleanup;
+    end = apr_time_now() + max_wait;
+
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p,
+                  "acquire global lock: %s", lpath);
+    while (apr_time_now() < end) {
+        rv = apr_file_open(&s_fs->global_lock, lpath,
+                           (APR_FOPEN_WRITE|APR_FOPEN_CREATE),
+                           MD_FPROT_F_UALL_GREAD, p);
+        if (APR_SUCCESS != rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p,
+                          "unable to create/open lock file: %s",
+                          lpath);
+            goto next_try;
+        }
+        rv = apr_file_lock(s_fs->global_lock,
+                           APR_FLOCK_EXCLUSIVE|APR_FLOCK_NONBLOCK);
+        if (APR_SUCCESS == rv) {
+            goto cleanup;
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p,
+                      "unable to obtain lock on: %s",
+                      lpath);
+
+    next_try:
+        if (s_fs->global_lock) {
+            apr_file_close(s_fs->global_lock);
+            s_fs->global_lock = NULL;
+        }
+        apr_sleep(apr_time_from_msec(100));
+    }
+    rv = APR_EGENERAL;
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p,
+                  "acquire global lock: %s", lpath);
+
+cleanup:
+    return rv;
+}
+
+static void fs_unlock_global(md_store_t *store, apr_pool_t *p)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+
+    (void)p;
+    if (s_fs->global_lock) {
+        apr_file_close(s_fs->global_lock);
+        s_fs->global_lock = NULL;
+    }
+}
index 8b09e2ef11fb6420176d90e7869a0831cc1e9a47..9a8d5882635fe46ad23578bd37e2d5640a98c47a 100644 (file)
@@ -27,7 +27,7 @@
  * @macro
  * Version number of the md module as c string
  */
-#define MOD_MD_VERSION "2.4.17"
+#define MOD_MD_VERSION "2.4.19"
 
 /**
  * @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 0x020410
+#define MOD_MD_VERSION_NUM 0x020413
 
 #define MD_ACME_DEF_URL         "https://acme-v02.api.letsencrypt.org/directory"
 #define MD_TAILSCALE_DEF_URL    "file://localhost/var/run/tailscale/tailscaled.sock"
index 558669657cc1b2b24eca4ae0450fef5392cd4334..32dea4fb883baa0380cee904f7e8b1511acbfefb 100644 (file)
@@ -713,27 +713,6 @@ static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p,
     return rv;
 }
 
-static void load_staged_data(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p)
-{
-    apr_status_t rv;
-    md_t *md;
-    md_result_t *result;
-    int i;
-
-    for (i = 0; i < mc->mds->nelts; ++i) {
-        md = APR_ARRAY_IDX(mc->mds, i, md_t *);
-        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);
-        }
-        else if (!APR_STATUS_IS_ENOENT(rv)) {
-            ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10069)
-                         "%s: error loading staged set", md->name);
-        }
-    }
-}
-
 static apr_status_t check_invalid_duplicates(server_rec *base_server)
 {
     server_rec *s;
@@ -891,7 +870,8 @@ static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog,
     if (APR_SUCCESS != rv) goto leave;
 
     rv = md_reg_create(&mc->reg, p, store, mc->proxy_url, mc->ca_certs,
-                       mc->min_delay, mc->retry_failover);
+                       mc->min_delay, mc->retry_failover,
+                       mc->use_store_locks, mc->lock_wait_timeout);
     if (APR_SUCCESS != rv) {
         ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10072) "setup md registry");
         goto leave;
@@ -934,14 +914,24 @@ static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog,
     /*3*/
     if (APR_SUCCESS != (rv = link_mds_to_servers(mc, s, p))) goto leave;
     /*4*/
+    if (APR_SUCCESS != (rv = md_reg_lock_global(mc->reg, ptemp))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10398)
+                     "unable to obtain global registry lock, "
+                     "renewed certificates may remain inactive on "
+                     "this httpd instance!");
+        /* FIXME: or should we fail the server start/reload here? */
+        rv = APR_SUCCESS;
+        goto leave;
+    }
     if (APR_SUCCESS != (rv = md_reg_sync_start(mc->reg, mc->mds, ptemp))) {
         ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10073)
                      "syncing %d mds to registry", mc->mds->nelts);
         goto leave;
     }
     /*5*/
-    load_staged_data(mc, s, p);
+    md_reg_load_stagings(mc->reg, mc->mds, mc->env, p);
 leave:
+    md_reg_unlock_global(mc->reg, ptemp);
     return rv;
 }
 
index f096ad238fe641472d0fd873cd7086448547dea0..2f193005849f67d5777407bb8746ffd7893bc067 100644 (file)
@@ -86,6 +86,8 @@ static md_mod_conf_t defmc = {
     NULL,                      /* CA cert file to use */
     apr_time_from_sec(5),      /* minimum delay for retries */
     13,                        /* retry_failover after 14 errors, with 5s delay ~ half a day */
+    0,                         /* store locks, disabled by default */
+    apr_time_from_sec(5),      /* max time to wait to obaint a store lock */
 };
 
 static md_timeslice_t def_renew_window = {
@@ -647,6 +649,36 @@ static const char *md_config_set_retry_failover(cmd_parms *cmd, void *dc, const
     return NULL;
 }
 
+static const char *md_config_set_store_locks(cmd_parms *cmd, void *dc, const char *s)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD);
+    int use_store_locks;
+    apr_time_t wait_time = 0;
+
+    (void)dc;
+    if (err) {
+        return err;
+    }
+    else if (!apr_strnatcasecmp("off", s)) {
+        use_store_locks = 0;
+    }
+    else if (!apr_strnatcasecmp("on", s)) {
+        use_store_locks = 1;
+    }
+    else {
+        if (md_duration_parse(&wait_time, s, "s") != APR_SUCCESS) {
+            return "neither 'on', 'off' or a duration specified";
+        }
+        use_store_locks = (wait_time != 0);
+    }
+    config->mc->use_store_locks = use_store_locks;
+    if (wait_time) {
+        config->mc->lock_wait_timeout = wait_time;
+    }
+    return NULL;
+}
+
 static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value)
 {
     md_srv_conf_t *config = md_config_get(cmd->server);
@@ -1215,6 +1247,8 @@ const command_rec md_cmds[] = {
                   "Time length for first retry, doubled on every consecutive error."),
     AP_INIT_TAKE1("MDRetryFailover", md_config_set_retry_failover, NULL, RSRC_CONF,
                   "The number of errors before a failover to another CA is triggered."),
+    AP_INIT_TAKE1("MDStoreLocks", md_config_set_store_locks, NULL, RSRC_CONF,
+                  "Configure locking of store for updates."),
 
     AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
 };
index 5d7da4b8d1de662f3a2454436bb7d541865dae39..b34b92e14c4caf82349594b384f385e19ce85f67 100644 (file)
@@ -72,6 +72,8 @@ struct md_mod_conf_t {
     const char *ca_certs;              /* root certificates to use for connections */
     apr_time_t min_delay;              /* minimum delay for retries */
     int retry_failover;                /* number of errors to trigger CA failover */
+    int use_store_locks;               /* use locks when updating store */
+    apr_time_t lock_wait_timeout;      /* fail after this time when unable to obtain lock */
 };
 
 typedef struct md_srv_conf_t {
index fc3206db108d15fa5f337959d51191d66e4acdb3..a25ea1aae4c91790d0c493057c8843a37c1428ca 100755 (executable)
@@ -50,6 +50,7 @@ def _session_scope(env):
         'AH01909',  # mod_ssl, cert alt name complains
         'AH10170',  # mod_md, wrong config, tested
         'AH10171',  # mod_md, wrong config, tested
+        'AH10398',  # test on global store lock
     ])
 
     env.httpd_error_log.add_ignored_patterns([
@@ -60,6 +61,7 @@ def _session_scope(env):
         re.compile(r'.*problem\[urn:org:apache:httpd:log:AH\d+:].*'),
         re.compile(r'.*Unsuccessful in contacting ACME server at :*'),
         re.compile(r'.*test-md-720-002-\S+.org: dns-01 setup command failed .*'),
+        re.compile(r'.*AH\d*: unable to obtain global registry lock, .*'),
     ])
     if env.lacks_ocsp():
         env.httpd_error_log.add_ignored_patterns([
diff --git a/test/modules/md/test_820_locks.py b/test/modules/md/test_820_locks.py
new file mode 100644 (file)
index 0000000..f7dde6a
--- /dev/null
@@ -0,0 +1,72 @@
+import os
+
+import pytest
+from filelock import Timeout, FileLock
+
+from .md_cert_util import MDCertUtil
+from .md_conf import MDConf
+from .md_env import MDTestEnv
+
+
+@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(),
+                    reason="no ACME test server configured")
+class TestLocks:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env, acme):
+        env.APACHE_CONF_SRC = "data/test_auto"
+        acme.start(config='default')
+        env.check_acme()
+        env.clear_store()
+
+    @pytest.fixture(autouse=True, scope='function')
+    def _method_scope(self, env, request):
+        env.clear_store()
+        self.test_domain = env.get_request_domain(request)
+
+    def configure_httpd(self, env, domains, add_lines=""):
+        conf = MDConf(env)
+        conf.add(add_lines)
+        conf.add_md(domains)
+        conf.add_vhost(domains)
+        conf.install()
+
+    # normal renewal with store locks activated
+    def test_md_820_001(self, env):
+        domain = self.test_domain
+        self.configure_httpd(env, [domain], add_lines=[
+            "MDStoreLocks 1s"
+        ])
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+
+    # renewal, with global lock held during restert
+    def test_md_820_002(self, env):
+        domain = self.test_domain
+        self.configure_httpd(env, [domain], add_lines=[
+            "MDStoreLocks 1s"
+        ])
+        assert env.apache_restart() == 0
+        assert env.await_completion([domain])
+        # we have a cert now, add a dns name to force renewal
+        certa = MDCertUtil(env.store_domain_file(domain, 'pubcert.pem'))
+        self.configure_httpd(env, [domain, f"x.{domain}"], add_lines=[
+            "MDStoreLocks 1s"
+        ])
+        assert env.apache_restart() == 0
+        # await new cert, but do not restart, keeps the cert in staging
+        assert env.await_completion([domain], restart=False)
+        # obtain global lock and restart
+        lockfile = os.path.join(env.store_dir, "store.lock")
+        with FileLock(lockfile):
+            assert env.apache_restart() == 0
+        # lock should have prevented staging from being activated,
+        # meaning we will have the same cert
+        certb = MDCertUtil(env.store_domain_file(domain, 'pubcert.pem'))
+        assert certa.same_serial_as(certb)
+        # now restart without lock
+        assert env.apache_restart() == 0
+        certc = MDCertUtil(env.store_domain_file(domain, 'pubcert.pem'))
+        assert not certa.same_serial_as(certc)
+
+