summaryrefslogtreecommitdiffstats
path: root/modules/md
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:01:30 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:01:30 +0000
commit6beeb1b708550be0d4a53b272283e17e5e35fe17 (patch)
tree1ce8673d4aaa948e5554000101f46536a1e4cc29 /modules/md
parentInitial commit. (diff)
downloadapache2-6beeb1b708550be0d4a53b272283e17e5e35fe17.tar.xz
apache2-6beeb1b708550be0d4a53b272283e17e5e35fe17.zip
Adding upstream version 2.4.57.upstream/2.4.57
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/md')
-rw-r--r--modules/md/Makefile.in20
-rw-r--r--modules/md/config2.m4311
-rw-r--r--modules/md/md.h330
-rw-r--r--modules/md/md_acme.c797
-rw-r--r--modules/md/md_acme.h317
-rw-r--r--modules/md/md_acme_acct.c749
-rw-r--r--modules/md/md_acme_acct.h148
-rw-r--r--modules/md/md_acme_authz.c697
-rw-r--r--modules/md/md_acme_authz.h79
-rw-r--r--modules/md/md_acme_drive.c1106
-rw-r--r--modules/md/md_acme_drive.h55
-rw-r--r--modules/md/md_acme_order.c562
-rw-r--r--modules/md/md_acme_order.h91
-rw-r--r--modules/md/md_acmev2_drive.c181
-rw-r--r--modules/md/md_acmev2_drive.h27
-rw-r--r--modules/md/md_core.c462
-rw-r--r--modules/md/md_crypt.c2140
-rw-r--r--modules/md/md_crypt.h253
-rw-r--r--modules/md/md_curl.c653
-rw-r--r--modules/md/md_curl.h24
-rw-r--r--modules/md/md_event.c89
-rw-r--r--modules/md/md_event.h46
-rw-r--r--modules/md/md_http.c397
-rw-r--r--modules/md/md_http.h272
-rw-r--r--modules/md/md_json.c1311
-rw-r--r--modules/md/md_json.h157
-rw-r--r--modules/md/md_jws.c148
-rw-r--r--modules/md/md_jws.h52
-rw-r--r--modules/md/md_log.c78
-rw-r--r--modules/md/md_log.h60
-rw-r--r--modules/md/md_ocsp.c1063
-rw-r--r--modules/md/md_ocsp.h71
-rw-r--r--modules/md/md_reg.c1323
-rw-r--r--modules/md/md_reg.h313
-rw-r--r--modules/md/md_result.c285
-rw-r--r--modules/md/md_result.h87
-rw-r--r--modules/md/md_status.c653
-rw-r--r--modules/md/md_status.h126
-rw-r--r--modules/md/md_store.c385
-rw-r--r--modules/md/md_store.h343
-rw-r--r--modules/md/md_store_fs.c1169
-rw-r--r--modules/md/md_store_fs.h65
-rw-r--r--modules/md/md_tailscale.c383
-rw-r--r--modules/md/md_tailscale.h25
-rw-r--r--modules/md/md_time.c325
-rw-r--r--modules/md/md_time.h77
-rw-r--r--modules/md/md_util.c1561
-rw-r--r--modules/md/md_util.h253
-rw-r--r--modules/md/md_version.h43
-rw-r--r--modules/md/mod_md.c1535
-rw-r--r--modules/md/mod_md.dep5
-rw-r--r--modules/md/mod_md.dsp223
-rw-r--r--modules/md/mod_md.h20
-rw-r--r--modules/md/mod_md.mak618
-rw-r--r--modules/md/mod_md_config.c1388
-rw-r--r--modules/md/mod_md_config.h132
-rw-r--r--modules/md/mod_md_drive.c345
-rw-r--r--modules/md/mod_md_drive.h35
-rw-r--r--modules/md/mod_md_ocsp.c272
-rw-r--r--modules/md/mod_md_ocsp.h33
-rw-r--r--modules/md/mod_md_os.c88
-rw-r--r--modules/md/mod_md_os.h37
-rw-r--r--modules/md/mod_md_private.h24
-rw-r--r--modules/md/mod_md_status.c987
-rw-r--r--modules/md/mod_md_status.h27
65 files changed, 25931 insertions, 0 deletions
diff --git a/modules/md/Makefile.in b/modules/md/Makefile.in
new file mode 100644
index 0000000..4395bc3
--- /dev/null
+++ b/modules/md/Makefile.in
@@ -0,0 +1,20 @@
+# 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.
+
+#
+# standard stuff
+#
+
+include $(top_srcdir)/build/special.mk
diff --git a/modules/md/config2.m4 b/modules/md/config2.m4
new file mode 100644
index 0000000..11d4f32
--- /dev/null
+++ b/modules/md/config2.m4
@@ -0,0 +1,311 @@
+dnl Licensed to the Apache Software Foundation (ASF) under one or more
+dnl contributor license agreements. See the NOTICE file distributed with
+dnl this work for additional information regarding copyright ownership.
+dnl The ASF licenses this file to You under the Apache License, Version 2.0
+dnl (the "License"); you may not use this file except in compliance with
+dnl the License. You may obtain a copy of the License at
+dnl
+dnl http://www.apache.org/licenses/LICENSE-2.0
+dnl
+dnl Unless required by applicable law or agreed to in writing, software
+dnl distributed under the License is distributed on an "AS IS" BASIS,
+dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+dnl See the License for the specific language governing permissions and
+dnl limitations under the License.
+
+dnl
+dnl APACHE_CHECK_CURL
+dnl
+dnl Configure for libcurl, giving preference to
+dnl "--with-curl=<path>" if it was specified.
+dnl
+AC_DEFUN([APACHE_CHECK_CURL],[
+ AC_CACHE_CHECK([for curl], [ac_cv_curl], [
+ dnl initialise the variables we use
+ ac_cv_curl=no
+ ap_curl_found=""
+ ap_curl_base=""
+ ap_curl_libs=""
+
+ dnl Determine the curl base directory, if any
+ AC_MSG_CHECKING([for user-provided curl base directory])
+ AC_ARG_WITH(curl, APACHE_HELP_STRING(--with-curl=PATH, curl installation directory), [
+ dnl If --with-curl specifies a directory, we use that directory
+ if test "x$withval" != "xyes" -a "x$withval" != "x"; then
+ dnl This ensures $withval is actually a directory and that it is absolute
+ ap_curl_base="`cd $withval ; pwd`"
+ fi
+ ])
+ if test "x$ap_curl_base" = "x"; then
+ AC_MSG_RESULT(none)
+ else
+ AC_MSG_RESULT($ap_curl_base)
+ fi
+
+ dnl Run header and version checks
+ saved_CPPFLAGS="$CPPFLAGS"
+ saved_LIBS="$LIBS"
+ saved_LDFLAGS="$LDFLAGS"
+
+ dnl Before doing anything else, load in pkg-config variables
+ if test -n "$PKGCONFIG"; then
+ saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH"
+ AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH])
+ if test "x$ap_curl_base" != "x" ; then
+ if test -f "${ap_curl_base}/lib/pkgconfig/libcurl.pc"; then
+ dnl Ensure that the given path is used by pkg-config too, otherwise
+ dnl the system libcurl.pc might be picked up instead.
+ PKG_CONFIG_PATH="${ap_curl_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+ export PKG_CONFIG_PATH
+ elif test -f "${ap_curl_base}/lib64/pkgconfig/libcurl.pc"; then
+ dnl Ensure that the given path is used by pkg-config too, otherwise
+ dnl the system libcurl.pc might be picked up instead.
+ PKG_CONFIG_PATH="${ap_curl_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+ export PKG_CONFIG_PATH
+ fi
+ fi
+ AC_ARG_ENABLE(curl-staticlib-deps,APACHE_HELP_STRING(--enable-curl-staticlib-deps,[link mod_md with dependencies of libcurl's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-md.]), [
+ if test "$enableval" = "yes"; then
+ PKGCONFIG_LIBOPTS="--static"
+ fi
+ ])
+ ap_curl_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libcurl`"
+ if test $? -eq 0; then
+ ap_curl_found="yes"
+ pkglookup="`$PKGCONFIG --cflags-only-I libcurl`"
+ APR_ADDTO(CPPFLAGS, [$pkglookup])
+ APR_ADDTO(MOD_CFLAGS, [$pkglookup])
+ pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libcurl`"
+ APR_ADDTO(LDFLAGS, [$pkglookup])
+ APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+ pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libcurl`"
+ APR_ADDTO(LDFLAGS, [$pkglookup])
+ APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+ fi
+ PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH"
+ fi
+
+ dnl fall back to the user-supplied directory if not found via pkg-config
+ if test "x$ap_curl_base" != "x" -a "x$ap_curl_found" = "x"; then
+ APR_ADDTO(CPPFLAGS, [-I$ap_curl_base/include])
+ APR_ADDTO(MOD_CFLAGS, [-I$ap_curl_base/include])
+ APR_ADDTO(LDFLAGS, [-L$ap_curl_base/lib])
+ APR_ADDTO(MOD_LDFLAGS, [-L$ap_curl_base/lib])
+ if test "x$ap_platform_runtime_link_flag" != "x"; then
+ APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_curl_base/lib])
+ APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_curl_base/lib])
+ fi
+ fi
+
+ AC_CHECK_HEADERS([curl/curl.h])
+
+ AC_MSG_CHECKING([for curl version >= 7.29])
+ AC_TRY_COMPILE([#include <curl/curlver.h>],[
+#if !defined(LIBCURL_VERSION_MAJOR)
+#error "Missing libcurl version"
+#endif
+#if LIBCURL_VERSION_MAJOR < 7
+#error "Unsupported libcurl version " LIBCURL_VERSION
+#endif
+#if LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 29
+#error "Unsupported libcurl version " LIBCURL_VERSION
+#endif],
+ [AC_MSG_RESULT(OK)
+ ac_cv_curl=yes],
+ [AC_MSG_RESULT(FAILED)])
+
+ if test "x$ac_cv_curl" = "xyes"; then
+ ap_curl_libs="${ap_curl_libs:--lcurl} `$apr_config --libs`"
+ APR_ADDTO(MOD_LDFLAGS, [$ap_curl_libs])
+ APR_ADDTO(LIBS, [$ap_curl_libs])
+ fi
+
+ dnl restore
+ CPPFLAGS="$saved_CPPFLAGS"
+ LIBS="$saved_LIBS"
+ LDFLAGS="$saved_LDFLAGS"
+ ])
+ if test "x$ac_cv_curl" = "xyes"; then
+ AC_DEFINE(HAVE_CURL, 1, [Define if curl is available])
+ fi
+])
+
+
+dnl
+dnl APACHE_CHECK_JANSSON
+dnl
+dnl Configure for libjansson, giving preference to
+dnl "--with-jansson=<path>" if it was specified.
+dnl
+AC_DEFUN([APACHE_CHECK_JANSSON],[
+ AC_CACHE_CHECK([for jansson], [ac_cv_jansson], [
+ dnl initialise the variables we use
+ ac_cv_jansson=no
+ ap_jansson_found=""
+ ap_jansson_base=""
+ ap_jansson_libs=""
+
+ dnl Determine the jansson base directory, if any
+ AC_MSG_CHECKING([for user-provided jansson base directory])
+ AC_ARG_WITH(jansson, APACHE_HELP_STRING(--with-jansson=PATH, jansson installation directory), [
+ dnl If --with-jansson specifies a directory, we use that directory
+ if test "x$withval" != "xyes" -a "x$withval" != "x"; then
+ dnl This ensures $withval is actually a directory and that it is absolute
+ ap_jansson_base="`cd $withval ; pwd`"
+ fi
+ ])
+ if test "x$ap_jansson_base" = "x"; then
+ AC_MSG_RESULT(none)
+ else
+ AC_MSG_RESULT($ap_jansson_base)
+ fi
+
+ dnl Run header and version checks
+ saved_CPPFLAGS="$CPPFLAGS"
+ saved_LIBS="$LIBS"
+ saved_LDFLAGS="$LDFLAGS"
+
+ dnl Before doing anything else, load in pkg-config variables
+ if test -n "$PKGCONFIG"; then
+ saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH"
+ AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH])
+ if test "x$ap_jansson_base" != "x" ; then
+ if test -f "${ap_jansson_base}/lib/pkgconfig/libjansson.pc"; then
+ dnl Ensure that the given path is used by pkg-config too, otherwise
+ dnl the system libjansson.pc might be picked up instead.
+ PKG_CONFIG_PATH="${ap_jansson_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+ export PKG_CONFIG_PATH
+ elif test -f "${ap_jansson_base}/lib64/pkgconfig/libjansson.pc"; then
+ dnl Ensure that the given path is used by pkg-config too, otherwise
+ dnl the system libjansson.pc might be picked up instead.
+ PKG_CONFIG_PATH="${ap_jansson_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+ export PKG_CONFIG_PATH
+ fi
+ fi
+ AC_ARG_ENABLE(jansson-staticlib-deps,APACHE_HELP_STRING(--enable-jansson-staticlib-deps,[link mod_md with dependencies of libjansson's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-md.]), [
+ if test "$enableval" = "yes"; then
+ PKGCONFIG_LIBOPTS="--static"
+ fi
+ ])
+ ap_jansson_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libjansson`"
+ if test $? -eq 0; then
+ ap_jansson_found="yes"
+ pkglookup="`$PKGCONFIG --cflags-only-I libjansson`"
+ APR_ADDTO(CPPFLAGS, [$pkglookup])
+ APR_ADDTO(MOD_CFLAGS, [$pkglookup])
+ pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libjansson`"
+ APR_ADDTO(LDFLAGS, [$pkglookup])
+ APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+ pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libjansson`"
+ APR_ADDTO(LDFLAGS, [$pkglookup])
+ APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+ fi
+ PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH"
+ fi
+
+ dnl fall back to the user-supplied directory if not found via pkg-config
+ if test "x$ap_jansson_base" != "x" -a "x$ap_jansson_found" = "x"; then
+ APR_ADDTO(CPPFLAGS, [-I$ap_jansson_base/include])
+ APR_ADDTO(MOD_CFLAGS, [-I$ap_jansson_base/include])
+ APR_ADDTO(LDFLAGS, [-L$ap_jansson_base/lib])
+ APR_ADDTO(MOD_LDFLAGS, [-L$ap_jansson_base/lib])
+ if test "x$ap_platform_runtime_link_flag" != "x"; then
+ APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_jansson_base/lib])
+ APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_jansson_base/lib])
+ fi
+ fi
+
+ # attempts to include jansson.h fail me. So lets make sure we can at least
+ # include its other header file
+ AC_TRY_COMPILE([#include <jansson_config.h>],[],
+ [AC_MSG_RESULT(OK)
+ ac_cv_jansson=yes],
+ [AC_MSG_RESULT(FAILED)])
+
+ if test "x$ac_cv_jansson" = "xyes"; then
+ ap_jansson_libs="${ap_jansson_libs:--ljansson} `$apr_config --libs`"
+ APR_ADDTO(MOD_LDFLAGS, [$ap_jansson_libs])
+ APR_ADDTO(LIBS, [$ap_jansson_libs])
+ fi
+
+ dnl restore
+ CPPFLAGS="$saved_CPPFLAGS"
+ LIBS="$saved_LIBS"
+ LDFLAGS="$saved_LDFLAGS"
+ ])
+ if test "x$ac_cv_jansson" = "xyes"; then
+ AC_DEFINE(HAVE_JANSSON, 1, [Define if jansson is available])
+ fi
+])
+
+
+dnl # start of module specific part
+APACHE_MODPATH_INIT(md)
+
+dnl # list of module object files
+md_objs="dnl
+md_acme.lo dnl
+md_acme_acct.lo dnl
+md_acme_authz.lo dnl
+md_acme_drive.lo dnl
+md_acmev2_drive.lo dnl
+md_acme_order.lo dnl
+md_core.lo dnl
+md_curl.lo dnl
+md_crypt.lo dnl
+md_event.lo dnl
+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
+md_store.lo dnl
+md_store_fs.lo dnl
+md_tailscale.lo dnl
+md_time.lo dnl
+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
+"
+
+# Ensure that other modules can pick up mod_md.h
+APR_ADDTO(INCLUDES, [-I\$(top_srcdir)/$modpath_current])
+
+dnl # hook module into the Autoconf mechanism (--enable-md)
+APACHE_MODULE(md, [Managed Domain handling], $md_objs, , most, [
+ APACHE_CHECK_OPENSSL
+ if test "x$ac_cv_openssl" = "xno" ; then
+ AC_MSG_WARN([libssl (or compatible) not found])
+ enable_md=no
+ fi
+
+ APACHE_CHECK_JANSSON
+ if test "x$ac_cv_jansson" != "xyes" ; then
+ AC_MSG_WARN([libjansson not found])
+ enable_md=no
+ fi
+
+ APACHE_CHECK_CURL
+ if test "x$ac_cv_curl" != "xyes" ; then
+ AC_MSG_WARN([libcurl not found])
+ enable_md=no
+ fi
+
+ AC_CHECK_FUNCS([arc4random_buf],
+ [APR_ADDTO(MOD_CPPFLAGS, ["-DMD_HAVE_ARC4RANDOM"])], [])
+
+ if test "x$enable_md" = "xshared"; then
+ APR_ADDTO(MOD_MD_LDADD, [-export-symbols-regex md_module])
+ fi
+])
+
+dnl # end of module specific part
+APACHE_MODPATH_FINISH
+
diff --git a/modules/md/md.h b/modules/md/md.h
new file mode 100644
index 0000000..1d75d10
--- /dev/null
+++ b/modules/md/md.h
@@ -0,0 +1,330 @@
+/* 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_h
+#define mod_md_md_h
+
+#include <apr_time.h>
+
+#include "md_time.h"
+#include "md_version.h"
+
+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;
+
+#define MD_PKEY_RSA_BITS_MIN 2048
+#define MD_PKEY_RSA_BITS_DEF 2048
+
+/* Minimum age for the HSTS header (RFC 6797), considered appropriate by Mozilla Security */
+#define MD_HSTS_HEADER "Strict-Transport-Security"
+#define MD_HSTS_MAX_AGE_DEFAULT 15768000
+
+#define PROTO_ACME_TLS_1 "acme-tls/1"
+
+#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 */
+ MD_S_INCOMPLETE = 1, /* MD is missing necessary information, cannot go live */
+ MD_S_COMPLETE = 2, /* MD has all necessary information, can go live */
+ MD_S_EXPIRED_DEPRECATED = 3, /* deprecated */
+ MD_S_ERROR = 4, /* MD data is flawed, unable to be processed as is */
+ MD_S_MISSING_INFORMATION = 5, /* User has not agreed to ToS */
+} md_state_t;
+
+typedef enum {
+ MD_REQUIRE_UNSET = -1,
+ MD_REQUIRE_OFF,
+ MD_REQUIRE_TEMPORARY,
+ MD_REQUIRE_PERMANENT,
+} md_require_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 */
+ MD_RENEW_ALWAYS, /* always renewed by httpd, even if not necessary */
+} md_renew_mode_t;
+
+typedef struct md_t md_t;
+struct md_t {
+ const char *name; /* unique name of this MD */
+ struct apr_array_header_t *domains; /* all DNS names this MD includes */
+ struct apr_array_header_t *contacts; /* list of contact uris, e.g. mailto:xxx */
+
+ int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */
+ md_require_t require_https; /* Iff https: is required for this MD */
+
+ int renew_mode; /* mode of obtaining credentials */
+ struct md_pkeys_spec_t *pks; /* specification for generating private keys */
+ int must_staple; /* certificates should set the OCSP Must Staple extension */
+ 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_proto; /* protocol used vs CA (e.g. ACME) */
+ struct apr_array_header_t *ca_urls; /* urls of CAs */
+ const char *ca_effective; /* url of CA used */
+ const char *ca_account; /* account used at CA */
+ const char *ca_agreement; /* accepted agreement uri between CA and user */
+ struct apr_array_header_t *ca_challenges; /* challenge types configured for this MD */
+ struct apr_array_header_t *cert_files; /* != NULL iff pubcerts explicitly configured */
+ struct apr_array_header_t *pkey_files; /* != NULL iff privkeys explicitly configured */
+ const char *ca_eab_kid; /* optional KEYID for external account binding */
+ const char *ca_eab_hmac; /* optional HMAC for external account binding */
+
+ md_state_t state; /* state of this MD */
+ const char *state_descr; /* description of state of NULL */
+
+ struct apr_array_header_t *acme_tls_1_domains; /* domains supporting "acme-tls/1" protocol */
+ int stapling; /* if OCSP stapling is enabled */
+ const char *dns01_cmd; /* DNS challenge command, override global command */
+
+ 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 */
+
+ const char *configured_name; /* name this MD was configured with, if different */
+};
+
+#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"
+#define MD_KEY_BITS "bits"
+#define MD_KEY_CA "ca"
+#define MD_KEY_CA_URL "ca-url"
+#define MD_KEY_CERT "cert"
+#define MD_KEY_CERT_FILES "cert-files"
+#define MD_KEY_CERTIFICATE "certificate"
+#define MD_KEY_CHALLENGE "challenge"
+#define MD_KEY_CHALLENGES "challenges"
+#define MD_KEY_CMD_DNS01 "cmd-dns-01"
+#define MD_KEY_COMPLETE "complete"
+#define MD_KEY_CONTACT "contact"
+#define MD_KEY_CONTACTS "contacts"
+#define MD_KEY_CSR "csr"
+#define MD_KEY_CURVE "curve"
+#define MD_KEY_DETAIL "detail"
+#define MD_KEY_DISABLED "disabled"
+#define MD_KEY_DIR "dir"
+#define MD_KEY_DOMAIN "domain"
+#define MD_KEY_DOMAINS "domains"
+#define MD_KEY_EAB "eab"
+#define MD_KEY_EAB_REQUIRED "externalAccountRequired"
+#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_HMAC "hmac"
+#define MD_KEY_HTTP "http"
+#define MD_KEY_HTTPS "https"
+#define MD_KEY_ID "id"
+#define MD_KEY_IDENTIFIER "identifier"
+#define MD_KEY_KEY "key"
+#define MD_KEY_KID "kid"
+#define MD_KEY_KEYAUTHZ "keyAuthorization"
+#define MD_KEY_LAST "last"
+#define MD_KEY_LAST_RUN "last-run"
+#define MD_KEY_LOCATION "location"
+#define MD_KEY_LOG "log"
+#define MD_KEY_MDS "managed-domains"
+#define MD_KEY_MESSAGE "message"
+#define MD_KEY_MUST_STAPLE "must-staple"
+#define MD_KEY_NAME "name"
+#define MD_KEY_NEXT_RUN "next-run"
+#define MD_KEY_NOTIFIED "notified"
+#define MD_KEY_NOTIFIED_RENEWED "notified-renewed"
+#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"
+#define MD_KEY_PKEY_FILES "pkey-files"
+#define MD_KEY_PROBLEM "problem"
+#define MD_KEY_PROTO "proto"
+#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_STATE_DESCR "state-descr"
+#define MD_KEY_STATUS "status"
+#define MD_KEY_STORE "store"
+#define MD_KEY_SUBPROBLEMS "subproblems"
+#define MD_KEY_TEMPORARY "temporary"
+#define MD_KEY_TOS "termsOfService"
+#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_URLS "urls"
+#define MD_KEY_URI "uri"
+#define MD_KEY_VALID "valid"
+#define MD_KEY_VALID_FROM "valid-from"
+#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"
+
+/* Check if a string member of a new MD (n) has
+ * a value and if it differs from the old MD o
+ */
+#define MD_VAL_UPDATE(n,o,s) ((n)->s != (o)->s)
+#define MD_SVAL_UPDATE(n,o,s) ((n)->s && (!(o)->s || strcmp((n)->s, (o)->s)))
+
+/**
+ * Determine if the Managed Domain contains a specific domain name.
+ */
+int md_contains(const md_t *md, const char *domain, int case_sensitive);
+
+/**
+ * Determine if the names of the two managed domains overlap.
+ */
+int md_domains_overlap(const md_t *md1, const md_t *md2);
+
+/**
+ * Determine if the domain names are equal.
+ */
+int md_equal_domains(const md_t *md1, const md_t *md2, int case_sensitive);
+
+/**
+ * Determine if the domains in md1 contain all domains of md2.
+ */
+int md_contains_domains(const md_t *md1, const md_t *md2);
+
+/**
+ * Get one common domain name of the two managed domains or NULL.
+ */
+const char *md_common_name(const md_t *md1, const md_t *md2);
+
+/**
+ * Get the number of common domains.
+ */
+apr_size_t md_common_name_count(const md_t *md1, const md_t *md2);
+
+/**
+ * Look up a managed domain by its name.
+ */
+md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name);
+
+/**
+ * Look up a managed domain by a DNS name it contains.
+ */
+md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain);
+
+/**
+ * Find a managed domain, different from the given one, that has overlaps
+ * in the domain list.
+ */
+md_t *md_get_by_dns_overlap(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);
+
+/**
+ * Create a managed domain, given a list of domain names.
+ */
+md_t *md_create(apr_pool_t *p, struct apr_array_header_t *domains);
+
+/**
+ * Deep copy an md record into another pool.
+ */
+md_t *md_clone(apr_pool_t *p, const md_t *src);
+
+/**
+ * Shallow copy an md record into another pool.
+ */
+md_t *md_copy(apr_pool_t *p, const md_t *src);
+
+/**
+ * Convert the managed domain into a JSON representation and vice versa.
+ *
+ * This reads and writes the following information: name, domains, ca_url, ca_proto and state.
+ */
+struct md_json_t *md_to_json(const md_t *md, apr_pool_t *p);
+md_t *md_from_json(struct md_json_t *json, apr_pool_t *p);
+
+/**
+ * Same as md_to_json(), but with sensitive fields stripped.
+ */
+struct md_json_t *md_to_public_json(const md_t *md, apr_pool_t *p);
+
+int md_is_covered_by_alt_names(const md_t *md, const struct apr_array_header_t* alt_names);
+
+/* how many certificates this domain has/will eventually have. */
+int md_cert_count(const md_t *md);
+
+const char *md_get_ca_name_from_url(apr_pool_t *p, const char *url);
+apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const char *name);
+
+/**************************************************************************************************/
+/* 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;
+struct md_pubcert_t {
+ struct apr_array_header_t *certs; /* chain of const md_cert*, leaf cert first */
+ struct apr_array_header_t *alt_names; /* alt-names of leaf cert */
+ const char *cert_file; /* file path of chain */
+ const char *key_file; /* file path of key for leaf cert */
+};
+
+#define MD_OK(c) (APR_SUCCESS == (rv = c))
+
+#endif /* mod_md_md_h */
diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c
new file mode 100644
index 0000000..4366bf6
--- /dev/null
+++ b/modules/md/md_acme.c
@@ -0,0 +1,797 @@
+/* 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 <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_result.h"
+#include "md_util.h"
+#include "md_version.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+
+
+static const char *base_product= "-";
+
+typedef struct acme_problem_status_t acme_problem_status_t;
+
+struct acme_problem_status_t {
+ const char *type; /* the ACME error string */
+ apr_status_t rv; /* what Apache status code we give it */
+ int input_related; /* if error indicates wrong input value */
+};
+
+static acme_problem_status_t Problems[] = {
+ { "acme:error:badCSR", APR_EINVAL, 1 },
+ { "acme:error:badNonce", APR_EAGAIN, 0 },
+ { "acme:error:badSignatureAlgorithm", APR_EINVAL, 1 },
+ { "acme:error:externalAccountRequired", APR_EINVAL, 1 },
+ { "acme:error:invalidContact", APR_BADARG, 1 },
+ { "acme:error:unsupportedContact", APR_EGENERAL, 1 },
+ { "acme:error:malformed", APR_EINVAL, 1 },
+ { "acme:error:rateLimited", APR_BADARG, 0 },
+ { "acme:error:rejectedIdentifier", APR_BADARG, 1 },
+ { "acme:error:serverInternal", APR_EGENERAL, 0 },
+ { "acme:error:unauthorized", APR_EACCES, 0 },
+ { "acme:error:unsupportedIdentifier", APR_BADARG, 1 },
+ { "acme:error:userActionRequired", APR_EAGAIN, 0 },
+ { "acme:error:badRevocationReason", APR_EINVAL, 1 },
+ { "acme:error:caa", APR_EGENERAL, 0 },
+ { "acme:error:dns", APR_EGENERAL, 0 },
+ { "acme:error:connection", APR_EGENERAL, 0 },
+ { "acme:error:tls", APR_EGENERAL, 0 },
+ { "acme:error:incorrectResponse", APR_EGENERAL, 0 },
+};
+
+static apr_status_t problem_status_get(const char *type) {
+ size_t i;
+
+ if (strstr(type, "urn:ietf:params:") == type) {
+ type += strlen("urn:ietf:params:");
+ }
+ else if (strstr(type, "urn:") == type) {
+ type += strlen("urn:");
+ }
+
+ for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) {
+ if (!apr_strnatcasecmp(type, Problems[i].type)) {
+ return Problems[i].rv;
+ }
+ }
+ return APR_EGENERAL;
+}
+
+int md_acme_problem_is_input_related(const char *problem) {
+ size_t i;
+
+ if (!problem) return 0;
+ if (strstr(problem, "urn:ietf:params:") == problem) {
+ problem += strlen("urn:ietf:params:");
+ }
+ else if (strstr(problem, "urn:") == problem) {
+ problem += strlen("urn:");
+ }
+
+ for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) {
+ if (!apr_strnatcasecmp(problem, Problems[i].type)) {
+ return Problems[i].input_related;
+ }
+ }
+ return 0;
+}
+
+/**************************************************************************************************/
+/* acme requests */
+
+static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs)
+{
+ if (hdrs) {
+ const char *nonce = apr_table_get(hdrs, "Replay-Nonce");
+ if (nonce) {
+ acme->nonce = apr_pstrdup(acme->p, nonce);
+ }
+ }
+}
+
+static apr_status_t http_update_nonce(const md_http_response_t *res, void *data)
+{
+ req_update_nonce(data, res->headers);
+ return APR_SUCCESS;
+}
+
+static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, const char *url)
+{
+ apr_pool_t *pool;
+ md_acme_req_t *req;
+ apr_status_t rv;
+
+ rv = apr_pool_create(&pool, acme->p);
+ if (rv != APR_SUCCESS) {
+ return NULL;
+ }
+ apr_pool_tag(pool, "md_acme_req");
+
+ req = apr_pcalloc(pool, sizeof(*req));
+ if (!req) {
+ apr_pool_destroy(pool);
+ return NULL;
+ }
+
+ req->acme = acme;
+ req->p = pool;
+ req->method = method;
+ req->url = url;
+ req->prot_fields = md_json_create(pool);
+ req->max_retries = acme->max_retries;
+ req->result = md_result_make(req->p, APR_SUCCESS);
+ return req;
+}
+
+static apr_status_t acmev2_new_nonce(md_acme_t *acme)
+{
+ return md_http_HEAD_perform(acme->http, acme->api.v2.new_nonce, NULL, http_update_nonce, acme);
+}
+
+
+apr_status_t md_acme_init(apr_pool_t *p, const char *base, int init_ssl)
+{
+ base_product = base;
+ return init_ssl? md_crypt_init(p) : APR_SUCCESS;
+}
+
+static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res)
+{
+ const char *ctype;
+ md_json_t *problem = NULL;
+ apr_status_t rv;
+
+ ctype = apr_table_get(req->resp_hdrs, "content-type");
+ ctype = md_util_parse_ct(res->req->pool, ctype);
+ if (ctype && !strcmp(ctype, "application/problem+json")) {
+ /* RFC 7807 */
+ rv = md_json_read_http(&problem, req->p, res);
+ if (rv == APR_SUCCESS && problem) {
+ const char *ptype, *pdetail;
+
+ req->resp_json = problem;
+ 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_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,
+ "acme reports %s: %s", ptype, pdetail);
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, req->rv, req->p,
+ "acme problem %s: %s", ptype, pdetail);
+ }
+ return req->rv;
+ }
+ }
+
+ switch (res->status) {
+ case 400:
+ return APR_EINVAL;
+ case 401: /* sectigo returns this instead of 403 */
+ 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 APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* ACME requests with nonce handling */
+
+static apr_status_t acmev2_req_init(md_acme_req_t *req, md_json_t *jpayload)
+{
+ md_data_t payload;
+
+ md_data_null(&payload);
+ if (!req->acme->acct) {
+ return APR_EINVAL;
+ }
+ if (jpayload) {
+ payload.data = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT);
+ if (!payload.data) {
+ return APR_EINVAL;
+ }
+ }
+ else {
+ payload.data = "";
+ }
+
+ 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.data);
+ return md_jws_sign(&req->req_json, req->p, &payload,
+ req->prot_fields, req->acme->acct_key, req->acme->acct->url);
+}
+
+apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *payload)
+{
+ return req->acme->req_init_fn(req, payload);
+}
+
+static apr_status_t md_acme_req_done(md_acme_req_t *req, apr_status_t rv)
+{
+ if (req->result->status != APR_SUCCESS) {
+ if (req->on_err) {
+ req->on_err(req, req->result, req->baton);
+ }
+ }
+ /* An error in rv superceeds the result->status */
+ if (APR_SUCCESS != rv) req->result->status = rv;
+ rv = req->result->status;
+ /* transfer results into the acme's central result for longer life and later inspection */
+ md_result_dup(req->acme->last, req->result);
+ if (req->p) {
+ apr_pool_destroy(req->p);
+ }
+ return rv;
+}
+
+static apr_status_t on_response(const md_http_response_t *res, void *data)
+{
+ 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);
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->p, "response: %d", res->status);
+ if (res->status >= 200 && res->status < 300) {
+ int processed = 0;
+
+ if (req->on_json) {
+ processed = 1;
+ rv = md_json_read_http(&req->resp_json, req->p, res);
+ if (APR_SUCCESS == rv) {
+ if (md_log_is_level(req->p, MD_LOG_TRACE2)) {
+ const char *s;
+ s = md_json_writep(req->resp_json, req->p, MD_JSON_FMT_INDENT);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->p,
+ "response: %s",
+ s ? s : "<failed to serialize!>");
+ }
+ rv = req->on_json(req->acme, req->p, req->resp_hdrs, req->resp_json, req->baton);
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ /* not JSON content, fall through */
+ processed = 0;
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, "parsing JSON body");
+ }
+ }
+
+ if (!processed && req->on_res) {
+ processed = 1;
+ rv = req->on_res(req->acme, res, req->baton);
+ }
+
+ if (!processed) {
+ rv = APR_EINVAL;
+ md_result_printf(req->result, rv, "unable to process the response: "
+ "http-status=%d, content-type=%s",
+ res->status, apr_table_get(res->headers, "Content-Type"));
+ md_result_log(req->result, MD_LOG_ERR);
+ }
+ }
+ else if (APR_EAGAIN == (rv = inspect_problem(req, res))) {
+ /* leave req alive */
+ return rv;
+ }
+
+ md_acme_req_done(req, rv);
+ return rv;
+}
+
+static apr_status_t acmev2_GET_as_POST_init(md_acme_req_t *req, void *baton)
+{
+ (void)baton;
+ return md_acme_req_body_init(req, NULL);
+}
+
+static apr_status_t md_acme_req_send(md_acme_req_t *req)
+{
+ apr_status_t rv;
+ md_acme_t *acme = req->acme;
+ md_data_t *body = NULL;
+ md_result_t *result;
+
+ assert(acme->url);
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p,
+ "sending req: %s %s", req->method, req->url);
+ md_result_reset(req->acme->last);
+ result = md_result_make(req->p, APR_SUCCESS);
+
+ /* Whom are we talking to? */
+ if (acme->version == MD_ACME_VERSION_UNKNOWN) {
+ rv = md_acme_setup(acme, result);
+ if (APR_SUCCESS != rv) goto leave;
+ }
+
+ if (!strcmp("GET", req->method) && !req->on_init && !req->req_json) {
+ /* See <https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.6.3>
+ * and <https://mailarchive.ietf.org/arch/msg/acme/sotffSQ0OWV-qQJodLwWYWcEVKI>
+ * and <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380>
+ * We implement this change in ACMEv2 and higher as keeping the md_acme_GET() methods,
+ * but switching them to POSTs with a empty, JWS signed, body when we call
+ * our HTTP client. */
+ req->method = "POST";
+ req->on_init = acmev2_GET_as_POST_init;
+ /*req->max_retries = 0; don't do retries on these "GET"s */
+ }
+
+ /* Besides GET/HEAD, we always need a fresh nonce */
+ if (strcmp("GET", req->method) && strcmp("HEAD", req->method)) {
+ if (acme->version == MD_ACME_VERSION_UNKNOWN) {
+ rv = md_acme_setup(acme, result);
+ if (APR_SUCCESS != rv) goto leave;
+ }
+ if (!acme->nonce && (APR_SUCCESS != (rv = acme->new_nonce_fn(acme)))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, req->p,
+ "error retrieving new nonce from ACME server");
+ goto leave;
+ }
+
+ md_json_sets(acme->nonce, req->prot_fields, "nonce", NULL);
+ md_json_sets(req->url, req->prot_fields, "url", NULL);
+ acme->nonce = NULL;
+ }
+
+ rv = req->on_init? req->on_init(req, req->baton) : APR_SUCCESS;
+ if (APR_SUCCESS != rv) goto leave;
+
+ if (req->req_json) {
+ body = apr_pcalloc(req->p, sizeof(*body));
+ body->data = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT);
+ body->len = strlen(body->data);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->p,
+ "sending JSON body: %s", body->data);
+ }
+
+ if (body && md_log_is_level(req->p, MD_LOG_TRACE4)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->p,
+ "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,
+ "req: %s %s", req->method, req->url);
+ }
+
+ if (!strcmp("GET", req->method)) {
+ rv = md_http_GET_perform(req->acme->http, req->url, NULL, on_response, req);
+ }
+ else if (!strcmp("POST", req->method)) {
+ 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_perform(req->acme->http, req->url, NULL, on_response, req);
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, req->p,
+ "HTTP method %s against: %s", req->method, req->url);
+ rv = APR_ENOTIMPL;
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->p, "req sent");
+
+ if (APR_EAGAIN == rv && req->max_retries > 0) {
+ --req->max_retries;
+ rv = md_acme_req_send(req);
+ }
+ req = NULL;
+
+leave:
+ if (req) md_acme_req_done(req, rv);
+ return rv;
+}
+
+apr_status_t md_acme_POST(md_acme_t *acme, const char *url,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton)
+{
+ md_acme_req_t *req;
+
+ assert(url);
+ assert(on_json || on_res);
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme POST: %s", url);
+ req = md_acme_req_create(acme, "POST", url);
+ req->on_init = on_init;
+ req->on_json = on_json;
+ req->on_res = on_res;
+ req->on_err = on_err;
+ req->baton = baton;
+
+ return md_acme_req_send(req);
+}
+
+apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton)
+{
+ md_acme_req_t *req;
+
+ assert(url);
+ assert(on_json || on_res);
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme GET: %s", url);
+ req = md_acme_req_create(acme, "GET", url);
+ req->on_init = on_init;
+ req->on_json = on_json;
+ req->on_res = on_res;
+ req->on_err = on_err;
+ req->baton = baton;
+
+ return md_acme_req_send(req);
+}
+
+void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result)
+{
+ if (acme->last->status == APR_SUCCESS) {
+ md_result_set(result, rv, NULL);
+ }
+ else {
+ md_result_problem_set(result, acme->last->status, acme->last->problem,
+ acme->last->detail, acme->last->subproblems);
+ }
+}
+
+/**************************************************************************************************/
+/* GET JSON */
+
+typedef struct {
+ apr_pool_t *pool;
+ md_json_t *json;
+} json_ctx;
+
+static apr_status_t on_got_json(md_acme_t *acme, apr_pool_t *p, const apr_table_t *headers,
+ md_json_t *jbody, void *baton)
+{
+ json_ctx *ctx = baton;
+
+ (void)acme;
+ (void)p;
+ (void)headers;
+ ctx->json = md_json_clone(ctx->pool, jbody);
+ return APR_SUCCESS;
+}
+
+apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme,
+ const char *url, apr_pool_t *p)
+{
+ apr_status_t rv;
+ json_ctx ctx;
+
+ ctx.pool = p;
+ ctx.json = NULL;
+
+ rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, &ctx);
+ *pjson = (APR_SUCCESS == rv)? ctx.json : NULL;
+ return rv;
+}
+
+/**************************************************************************************************/
+/* Generic ACME operations */
+
+void md_acme_clear_acct(md_acme_t *acme)
+{
+ acme->acct_id = NULL;
+ acme->acct = NULL;
+ acme->acct_key = NULL;
+}
+
+const char *md_acme_acct_id_get(md_acme_t *acme)
+{
+ return acme->acct_id;
+}
+
+const char *md_acme_acct_url_get(md_acme_t *acme)
+{
+ return acme->acct? acme->acct->url : NULL;
+}
+
+apr_status_t md_acme_use_acct(md_acme_t *acme, md_store_t *store,
+ apr_pool_t *p, const char *acct_id)
+{
+ md_acme_acct_t *acct;
+ md_pkey_t *pkey;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey,
+ store, MD_SG_ACCOUNTS, acct_id, acme->p))) {
+ if (md_acme_acct_matches_url(acct, acme->url)) {
+ acme->acct_id = apr_pstrdup(p, acct_id);
+ acme->acct = acct;
+ acme->acct_key = pkey;
+ rv = md_acme_acct_validate(acme, store, p);
+ }
+ else {
+ /* account is from another server or, more likely, from another
+ * protocol endpoint on the same server */
+ rv = APR_ENOENT;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_acme_use_acct_for_md(md_acme_t *acme, struct md_store_t *store,
+ apr_pool_t *p, const char *acct_id,
+ const md_t *md)
+{
+ md_acme_acct_t *acct;
+ md_pkey_t *pkey;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey,
+ store, MD_SG_ACCOUNTS, acct_id, acme->p))) {
+ if (md_acme_acct_matches_md(acct, md)) {
+ acme->acct_id = apr_pstrdup(p, acct_id);
+ acme->acct = acct;
+ acme->acct_key = pkey;
+ rv = md_acme_acct_validate(acme, store, p);
+ }
+ else {
+ /* account is from another server or, more likely, from another
+ * protocol endpoint on the same server */
+ rv = APR_ENOENT;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_acme_save_acct(md_acme_t *acme, apr_pool_t *p, md_store_t *store)
+{
+ return md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key);
+}
+
+static apr_status_t acmev2_POST_new_account(md_acme_t *acme,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton)
+{
+ return md_acme_POST(acme, acme->api.v2.new_account, on_init, on_json, on_res, on_err, baton);
+}
+
+apr_status_t md_acme_POST_new_account(md_acme_t *acme,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton)
+{
+ return acme->post_new_account_fn(acme, on_init, on_json, on_res, on_err, baton);
+}
+
+/**************************************************************************************************/
+/* ACME setup */
+
+apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url,
+ const char *proxy_url, const char *ca_file)
+{
+ md_acme_t *acme;
+ const char *err = NULL;
+ apr_status_t rv;
+ apr_uri_t uri_parsed;
+ size_t len;
+
+ if (!url) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, "create ACME without url");
+ return APR_EINVAL;
+ }
+
+ if (APR_SUCCESS != (rv = md_util_abs_uri_check(p, url, &err))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "invalid ACME uri (%s): %s", err, url);
+ return rv;
+ }
+
+ acme = apr_pcalloc(p, sizeof(*acme));
+ acme->url = url;
+ acme->p = p;
+ acme->user_agent = apr_psprintf(p, "%s mod_md/%s",
+ base_product, MOD_MD_VERSION);
+ acme->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
+ acme->max_retries = 99;
+ acme->ca_file = ca_file;
+
+ if (APR_SUCCESS != (rv = apr_uri_parse(p, url, &uri_parsed))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "parsing ACME uri: %s", url);
+ return APR_EINVAL;
+ }
+
+ len = strlen(uri_parsed.hostname);
+ acme->sname = (len <= 16)? uri_parsed.hostname : apr_pstrdup(p, uri_parsed.hostname + len - 16);
+ acme->version = MD_ACME_VERSION_UNKNOWN;
+ acme->last = md_result_make(acme->p, APR_SUCCESS);
+
+ *pacme = acme;
+ return rv;
+}
+
+typedef struct {
+ md_acme_t *acme;
+ md_result_t *result;
+} update_dir_ctx;
+
+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 *)data)->acme;
+ md_result_t *result = ((update_dir_ctx *)data)->result;
+ apr_status_t rv;
+ md_json_t *json;
+ const char *s;
+
+ 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 "
+ "may happen during maintenance for short periods of time.", acme->url);
+ md_result_log(result, MD_LOG_INFO);
+ rv = result->status;
+ goto leave;
+ }
+ else if (res->status < 200 || res->status >= 300) {
+ md_result_printf(result, APR_EAGAIN,
+ "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);
+ rv = result->status;
+ goto leave;
+ }
+
+ rv = md_json_read_http(&json, req->pool, res);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->pool, "reading JSON body");
+ goto leave;
+ }
+
+ if (md_log_is_level(acme->p, MD_LOG_TRACE2)) {
+ s = md_json_writep(json, req->pool, MD_JSON_FMT_INDENT);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->pool,
+ "response: %s", s ? s : "<failed to serialize!>");
+ }
+
+ /* What have we got? */
+ if ((s = md_json_dups(acme->p, json, "newAccount", NULL))) {
+ acme->api.v2.new_account = s;
+ acme->api.v2.new_order = md_json_dups(acme->p, json, "newOrder", NULL);
+ acme->api.v2.revoke_cert = md_json_dups(acme->p, json, "revokeCert", NULL);
+ acme->api.v2.key_change = md_json_dups(acme->p, json, "keyChange", NULL);
+ acme->api.v2.new_nonce = md_json_dups(acme->p, json, "newNonce", NULL);
+ /* RFC 8555 only requires "directory" and "newNonce" resources.
+ * mod_md uses "newAccount" and "newOrder" so check for them.
+ * But mod_md does not use the "revokeCert" or "keyChange"
+ * resources, so tolerate the absence of those keys. In the
+ * future if mod_md implements revocation or key rollover then
+ * the use of those features should be predicated on the
+ * server's advertised capabilities. */
+ if (acme->api.v2.new_account
+ && acme->api.v2.new_order
+ && acme->api.v2.new_nonce) {
+ acme->version = MD_ACME_VERSION_2;
+ }
+ acme->ca_agreement = md_json_dups(acme->p, json, "meta", MD_KEY_TOS, NULL);
+ acme->eab_required = md_json_getb(json, "meta", MD_KEY_EAB_REQUIRED, NULL);
+ acme->new_nonce_fn = acmev2_new_nonce;
+ acme->req_init_fn = acmev2_req_init;
+ acme->post_new_account_fn = acmev2_POST_new_account;
+ }
+ else if ((s = md_json_dups(acme->p, json, "new-authz", NULL))) {
+ acme->api.v1.new_authz = s;
+ acme->api.v1.new_cert = md_json_dups(acme->p, json, "new-cert", NULL);
+ acme->api.v1.new_reg = md_json_dups(acme->p, json, "new-reg", NULL);
+ acme->api.v1.revoke_cert = md_json_dups(acme->p, json, "revoke-cert", NULL);
+ if (acme->api.v1.new_authz && acme->api.v1.new_cert
+ && acme->api.v1.new_reg && acme->api.v1.revoke_cert) {
+ acme->version = MD_ACME_VERSION_1;
+ }
+ acme->ca_agreement = md_json_dups(acme->p, json, "meta", "terms-of-service", NULL);
+ /* we init that far, but will not use the v1 api */
+ }
+
+ if (MD_ACME_VERSION_UNKNOWN == acme->version) {
+ md_result_printf(result, APR_EINVAL,
+ "Unable to understand ACME server response from <%s>. "
+ "Wrong ACME protocol version or link?", acme->url);
+ md_result_log(result, MD_LOG_WARNING);
+ rv = result->status;
+ }
+leave:
+ return rv;
+}
+
+apr_status_t md_acme_setup(md_acme_t *acme, md_result_t *result)
+{
+ apr_status_t rv;
+ update_dir_ctx ctx;
+
+ assert(acme->url);
+ acme->version = MD_ACME_VERSION_UNKNOWN;
+
+ if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p,
+ 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_http_set_ca_file(acme->http, acme->ca_file);
+
+ 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_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 */
+ md_result_printf(result, rv,
+ "Unsuccessful in contacting ACME server at <%s>. If this problem persists, "
+ "please check your network connectivity from your Apache server to the "
+ "ACME server. Also, older servers might have trouble verifying the certificates "
+ "of the ACME server. You can check if you are able to contact it manually via the "
+ "curl command. Sometimes, the ACME server might be down for maintenance, "
+ "so failing to contact it is not an immediate problem. Apache will "
+ "continue retrying this.", acme->url);
+ md_result_log(result, MD_LOG_WARNING);
+ }
+ return rv;
+}
+
+
diff --git a/modules/md/md_acme.h b/modules/md/md_acme.h
new file mode 100644
index 0000000..f28f2b6
--- /dev/null
+++ b/modules/md/md_acme.h
@@ -0,0 +1,317 @@
+/* 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_acme_h
+#define mod_md_md_acme_h
+
+struct apr_array_header_t;
+struct apr_bucket_brigade;
+struct md_http_response_t;
+struct apr_hash_t;
+struct md_http_t;
+struct md_json_t;
+struct md_pkey_t;
+struct md_t;
+struct md_acme_acct_t;
+struct md_acmev2_acct_t;
+struct md_store_t;
+struct md_result_t;
+
+#define MD_PROTO_ACME "ACME"
+
+#define MD_AUTHZ_CHA_HTTP_01 "http-01"
+#define MD_AUTHZ_CHA_SNI_01 "tls-sni-01"
+
+#define MD_ACME_VERSION_UNKNOWN 0x0
+#define MD_ACME_VERSION_1 0x010000
+#define MD_ACME_VERSION_2 0x020000
+
+#define MD_ACME_VERSION_MAJOR(i) (((i)&0xFF0000) >> 16)
+
+typedef enum {
+ MD_ACME_S_UNKNOWN, /* MD has not been analysed yet */
+ MD_ACME_S_REGISTERED, /* MD is registered at CA, but not more */
+ MD_ACME_S_TOS_ACCEPTED, /* Terms of Service were accepted by account holder */
+ MD_ACME_S_CHALLENGED, /* MD challenge information for all domains is known */
+ MD_ACME_S_VALIDATED, /* MD domains have been validated */
+ MD_ACME_S_CERTIFIED, /* MD has valid certificate */
+ MD_ACME_S_DENIED, /* MD domains (at least one) have been denied by CA */
+} md_acme_state_t;
+
+typedef struct md_acme_t md_acme_t;
+
+typedef struct md_acme_req_t md_acme_req_t;
+/**
+ * Request callback on a successful HTTP response (status 2xx).
+ */
+typedef apr_status_t md_acme_req_res_cb(md_acme_t *acme,
+ const struct md_http_response_t *res, void *baton);
+
+/**
+ * Request callback to initialize before sending. May be invoked more than once in
+ * case of retries.
+ */
+typedef apr_status_t md_acme_req_init_cb(md_acme_req_t *req, void *baton);
+
+/**
+ * Request callback on a successful response (HTTP response code 2xx) and content
+ * type matching application/.*json.
+ */
+typedef apr_status_t md_acme_req_json_cb(md_acme_t *acme, apr_pool_t *p,
+ const apr_table_t *headers,
+ struct md_json_t *jbody, void *baton);
+
+/**
+ * Request callback on detected errors.
+ */
+typedef apr_status_t md_acme_req_err_cb(md_acme_req_t *req,
+ const struct md_result_t *result, void *baton);
+
+
+typedef apr_status_t md_acme_new_nonce_fn(md_acme_t *acme);
+typedef apr_status_t md_acme_req_init_fn(md_acme_req_t *req, struct md_json_t *jpayload);
+
+typedef apr_status_t md_acme_post_fn(md_acme_t *acme,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton);
+
+struct md_acme_t {
+ const char *url; /* directory url of the ACME service */
+ const char *sname; /* short name for the service, not necessarily unique */
+ apr_pool_t *p;
+ const char *user_agent;
+ const char *proxy_url;
+ const char *ca_file;
+
+ const char *acct_id; /* local storage id account was loaded from or NULL */
+ struct md_acme_acct_t *acct; /* account at ACME server to use for requests */
+ struct md_pkey_t *acct_key; /* private RSA key belonging to account */
+
+ int version; /* as detected from the server */
+ union {
+ struct { /* obsolete */
+ const char *new_authz;
+ const char *new_cert;
+ const char *new_reg;
+ const char *revoke_cert;
+
+ } v1;
+ struct {
+ const char *new_account;
+ const char *new_order;
+ const char *key_change;
+ const char *revoke_cert;
+ const char *new_nonce;
+ } v2;
+ } api;
+ const char *ca_agreement;
+ const char *acct_name;
+ int eab_required;
+
+ md_acme_new_nonce_fn *new_nonce_fn;
+ md_acme_req_init_fn *req_init_fn;
+ md_acme_post_fn *post_new_account_fn;
+
+ struct md_http_t *http;
+
+ const char *nonce;
+ int max_retries;
+ struct md_result_t *last; /* result of last request */
+};
+
+/**
+ * Global init, call once at start up.
+ */
+apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version, int init_ssl);
+
+/**
+ * Create a new ACME server instance. If path is not NULL, will use that directory
+ * for persisting information. Will load any information persisted in earlier session.
+ * url needs only be specified for instances where this has never been persisted before.
+ *
+ * @param pacme will hold the ACME server instance on success
+ * @param p pool to used
+ * @param url url of the server, optional if known at path
+ * @param proxy_url optional url of a HTTP(S) proxy to use
+ */
+apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url,
+ const char *proxy_url, const char *ca_file);
+
+/**
+ * Contact the ACME server and retrieve its directory information.
+ *
+ * @param acme the ACME server to contact
+ */
+apr_status_t md_acme_setup(md_acme_t *acme, struct md_result_t *result);
+
+void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result);
+
+/**************************************************************************************************/
+/* account handling */
+
+/**
+ * Clear any existing account data from acme instance.
+ */
+void md_acme_clear_acct(md_acme_t *acme);
+
+apr_status_t md_acme_POST_new_account(md_acme_t *acme,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton);
+
+/**
+ * Get the local name of the account currently used by the acme instance.
+ * Will be NULL if no account has been setup successfully.
+ */
+const char *md_acme_acct_id_get(md_acme_t *acme);
+const char *md_acme_acct_url_get(md_acme_t *acme);
+
+/**
+ * Specify the account to use by name in local store. On success, the account
+ * is the "current" one used by the acme instance.
+ * @param acme the acme instance to set the account for
+ * @param store the store to load accounts from
+ * @param p pool for allocations
+ * @param acct_id name of the account to load
+ */
+apr_status_t md_acme_use_acct(md_acme_t *acme, struct md_store_t *store,
+ apr_pool_t *p, const char *acct_id);
+
+/**
+ * Specify the account to use for a specific MD by name in local store.
+ * On success, the account is the "current" one used by the acme instance.
+ * @param acme the acme instance to set the account for
+ * @param store the store to load accounts from
+ * @param p pool for allocations
+ * @param acct_id name of the account to load
+ * @param md the MD the account shall be used for
+ */
+apr_status_t md_acme_use_acct_for_md(md_acme_t *acme, struct md_store_t *store,
+ apr_pool_t *p, const char *acct_id,
+ const md_t *md);
+
+/**
+ * Get the local name of the account currently used by the acme instance.
+ * Will be NULL if no account has been setup successfully.
+ */
+const char *md_acme_acct_id_get(md_acme_t *acme);
+
+/**
+ * Agree to the given Terms-of-Service url for the current account.
+ */
+apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *tos);
+
+/**
+ * Confirm with the server that the current account agrees to the Terms-of-Service
+ * given in the agreement url.
+ * If the known agreement is equal to this, nothing is done.
+ * If it differs, the account is re-validated in the hope that the server
+ * announces the Tos URL it wants. If this is equal to the agreement specified,
+ * the server is notified of this. If the server requires a ToS that the account
+ * thinks it has already given, it is resend.
+ *
+ * If an agreement is required, different from the current one, APR_INCOMPLETE is
+ * returned and the agreement url is returned in the parameter.
+ */
+apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p,
+ const char *agreement, const char **prequired);
+
+apr_status_t md_acme_save_acct(md_acme_t *acme, apr_pool_t *p, struct md_store_t *store);
+
+/**
+ * Deactivate the current account at the ACME server..
+ */
+apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p);
+
+/**************************************************************************************************/
+/* request handling */
+
+struct md_acme_req_t {
+ md_acme_t *acme; /* the ACME server to talk to */
+ apr_pool_t *p; /* pool for the request duration */
+
+ const char *url; /* url to POST the request to */
+ const char *method; /* HTTP method to use */
+ struct md_json_t *prot_fields; /* JWS protected fields */
+ struct md_json_t *req_json; /* JSON to be POSTed in request body */
+
+ apr_table_t *resp_hdrs; /* HTTP response headers */
+ struct md_json_t *resp_json; /* JSON response body received */
+
+ apr_status_t rv; /* status of request */
+
+ md_acme_req_init_cb *on_init; /* callback to initialize the request before submit */
+ md_acme_req_json_cb *on_json; /* callback on successful JSON response */
+ md_acme_req_res_cb *on_res; /* callback on generic HTTP response */
+ md_acme_req_err_cb *on_err; /* callback on encountered error */
+ int max_retries; /* how often this might be retried */
+ void *baton; /* userdata for callbacks */
+ struct md_result_t *result; /* result of this request */
+};
+
+apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *payload);
+
+apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton);
+/**
+ * Perform a POST against the ACME url. If a on_json callback is given and
+ * the HTTP response is JSON, only this callback is invoked. Otherwise, on HTTP status
+ * 2xx, the on_res callback is invoked. If no on_res is given, it is considered a
+ * response error, since only JSON was expected.
+ * At least one callback needs to be non-NULL.
+ *
+ * @param acme the ACME server to talk to
+ * @param url the url to send the request to
+ * @param on_init callback to initialize the request data
+ * @param on_json callback on successful JSON response
+ * @param on_res callback on successful HTTP response
+ * @param baton userdata for callbacks
+ */
+apr_status_t md_acme_POST(md_acme_t *acme, const char *url,
+ md_acme_req_init_cb *on_init,
+ md_acme_req_json_cb *on_json,
+ md_acme_req_res_cb *on_res,
+ md_acme_req_err_cb *on_err,
+ void *baton);
+
+/**
+ * Retrieve a JSON resource from the ACME server
+ */
+apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme,
+ const char *url, apr_pool_t *p);
+
+
+apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *jpayload);
+
+apr_status_t md_acme_protos_add(struct apr_hash_t *protos, apr_pool_t *p);
+
+/**
+ * Return != 0 iff the given problem identifier is an ACME error string
+ * indicating something is wrong with the input values, e.g. from our
+ * configuration.
+ */
+int md_acme_problem_is_input_related(const char *problem);
+
+#endif /* md_acme_h */
diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c
new file mode 100644
index 0000000..f3e043e
--- /dev/null
+++ b/modules/md/md_acme_acct.c
@@ -0,0 +1,749 @@
+/* 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 <apr_lib.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_store.h"
+#include "md_util.h"
+#include "md_version.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+
+static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p,
+ const char *ca_url, apr_array_header_t *contacts)
+{
+ md_acme_acct_t *acct;
+
+ acct = apr_pcalloc(p, sizeof(*acct));
+ acct->ca_url = ca_url;
+ if (!contacts || apr_is_empty_array(contacts)) {
+ acct->contacts = apr_array_make(p, 5, sizeof(const char *));
+ }
+ else {
+ acct->contacts = apr_array_copy(p, contacts);
+ }
+
+ *pacct = acct;
+ return APR_SUCCESS;
+}
+
+
+static const char *mk_acct_id(apr_pool_t *p, md_acme_t *acme, int i)
+{
+ return apr_psprintf(p, "ACME-%s-%04d", acme->sname, i);
+}
+
+static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme)
+{
+ return apr_psprintf(p, "ACME-%s-*", acme->sname);
+}
+
+/**************************************************************************************************/
+/* json load/save */
+
+static md_acme_acct_st acct_st_from_str(const char *s)
+{
+ if (s) {
+ if (!strcmp("valid", s)) {
+ return MD_ACME_ACCT_ST_VALID;
+ }
+ else if (!strcmp("deactivated", s)) {
+ return MD_ACME_ACCT_ST_DEACTIVATED;
+ }
+ else if (!strcmp("revoked", s)) {
+ return MD_ACME_ACCT_ST_REVOKED;
+ }
+ }
+ return MD_ACME_ACCT_ST_UNKNOWN;
+}
+
+md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p)
+{
+ md_json_t *jacct;
+ const char *s;
+
+ assert(acct);
+ jacct = md_json_create(p);
+ switch (acct->status) {
+ case MD_ACME_ACCT_ST_VALID:
+ s = "valid";
+ break;
+ case MD_ACME_ACCT_ST_DEACTIVATED:
+ s = "deactivated";
+ break;
+ case MD_ACME_ACCT_ST_REVOKED:
+ s = "revoked";
+ break;
+ default:
+ s = NULL;
+ break;
+ }
+ 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);
+ if (acct->eab_kid) md_json_sets(acct->eab_kid, jacct, MD_KEY_EAB, MD_KEY_KID, NULL);
+ if (acct->eab_hmac) md_json_sets(acct->eab_hmac, jacct, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+
+ return jacct;
+}
+
+apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr_pool_t *p)
+{
+ apr_status_t rv = APR_EINVAL;
+ md_acme_acct_t *acct;
+ md_acme_acct_st status = MD_ACME_ACCT_ST_UNKNOWN;
+ const char *ca_url, *url;
+ apr_array_header_t *contacts;
+
+ if (md_json_has_key(json, MD_KEY_STATUS, NULL)) {
+ status = acct_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL));
+ }
+
+ url = md_json_gets(json, MD_KEY_URL, NULL);
+ if (!url) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url");
+ goto leave;
+ }
+
+ ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
+ if (!ca_url) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", url);
+ goto leave;
+ }
+
+ contacts = apr_array_make(p, 5, sizeof(const char *));
+ if (md_json_has_key(json, MD_KEY_CONTACT, NULL)) {
+ md_json_getsa(contacts, json, MD_KEY_CONTACT, NULL);
+ }
+ else {
+ md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL);
+ }
+ rv = acct_make(&acct, p, ca_url, contacts);
+ if (APR_SUCCESS != rv) goto leave;
+
+ acct->status = status;
+ acct->url = url;
+ acct->agreement = md_json_gets(json, MD_KEY_AGREEMENT, NULL);
+ if (!acct->agreement) {
+ /* backward compatible check */
+ acct->agreement = md_json_gets(json, "terms-of-service", NULL);
+ }
+ acct->orders = md_json_gets(json, MD_KEY_ORDERS, NULL);
+ if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_KID, NULL)
+ && md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) {
+ acct->eab_kid = md_json_gets(json, MD_KEY_EAB, MD_KEY_KID, NULL);
+ acct->eab_hmac = md_json_gets(json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+ }
+
+leave:
+ *pacct = (APR_SUCCESS == rv)? acct : NULL;
+ return rv;
+}
+
+apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme,
+ const char **pid, md_acme_acct_t *acct, md_pkey_t *acct_key)
+{
+ md_json_t *jacct;
+ apr_status_t rv;
+ int i;
+ const char *id = pid? *pid : NULL;
+
+ jacct = md_acme_acct_to_json(acct, p);
+ if (id) {
+ rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0);
+ }
+ else {
+ rv = APR_EAGAIN;
+ for (i = 0; i < 1000 && APR_SUCCESS != rv; ++i) {
+ id = mk_acct_id(p, acme, i);
+ rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1);
+ }
+ }
+ if (APR_SUCCESS == rv) {
+ if (pid) *pid = id;
+ rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCT_KEY, MD_SV_PKEY, acct_key, 0);
+ }
+ return rv;
+}
+
+apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey,
+ md_store_t *store, md_store_group_t group,
+ const char *name, apr_pool_t *p)
+{
+ md_json_t *json;
+ apr_status_t rv;
+
+ rv = md_store_load_json(store, group, name, MD_FN_ACCOUNT, &json, p);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ goto out;
+ }
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "error reading account: %s", name);
+ goto out;
+ }
+
+ rv = md_acme_acct_from_json(pacct, json, p);
+ if (APR_SUCCESS == rv) {
+ rv = md_store_load(store, group, name, MD_FN_ACCT_KEY, MD_SV_PKEY, (void**)ppkey, p);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "loading key: %s", name);
+ goto out;
+ }
+ }
+out:
+ if (APR_SUCCESS != rv) {
+ *pacct = NULL;
+ *ppkey = NULL;
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* Lookup */
+
+int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url)
+{
+ /* The ACME url must match exactly */
+ if (!url || !acct->ca_url || strcmp(acct->ca_url, url)) return 0;
+ return 1;
+}
+
+int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md)
+{
+ if (!md_acme_acct_matches_url(acct, md->ca_effective)) return 0;
+ /* if eab values are not mentioned, we match an account regardless
+ * if it was registered with eab or not */
+ if (!md->ca_eab_kid || !md->ca_eab_hmac) {
+ /* No eab only acceptable when no eab is asked for.
+ * This prevents someone that has no external account binding
+ * to re-use an account from another MDomain that was created
+ * with a binding. */
+ return !acct->eab_kid || !acct->eab_hmac;
+ }
+ /* But of eab is asked for, we need an acct that matches exactly.
+ * When someone configures a new EAB and we need
+ * to created a new account for it. */
+ if (!acct->eab_kid || !acct->eab_hmac) return 0;
+ return !strcmp(acct->eab_kid, md->ca_eab_kid)
+ && !strcmp(acct->eab_hmac, md->ca_eab_hmac);
+}
+
+typedef struct {
+ apr_pool_t *p;
+ const md_t *md;
+ const char *id;
+} find_ctx;
+
+static int find_acct(void *baton, const char *name, const char *aspect,
+ md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
+{
+ find_ctx *ctx = baton;
+ md_acme_acct_t *acct;
+ apr_status_t rv;
+
+ (void)aspect;
+ (void)ptemp;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, "account candidate %s/%s", name, aspect);
+ if (MD_SV_JSON == vtype) {
+ rv = md_acme_acct_from_json(&acct, (md_json_t*)value, ptemp);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ if (MD_ACME_ACCT_ST_VALID == acct->status
+ && (!ctx->md || md_acme_acct_matches_md(acct, ctx->md))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p,
+ "found account %s for %s: %s, status=%d",
+ acct->id, ctx->md->ca_effective, aspect, acct->status);
+ ctx->id = apr_pstrdup(ctx->p, name);
+ return 0;
+ }
+ }
+cleanup:
+ return 1;
+}
+
+static apr_status_t acct_find(const char **pid, md_acme_acct_t **pacct, md_pkey_t **ppkey,
+ md_store_t *store, md_store_group_t group,
+ const char *name_pattern,
+ const md_t *md, apr_pool_t *p)
+{
+ apr_status_t rv;
+ find_ctx ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.p = p;
+ ctx.md = md;
+
+ rv = md_store_iter(find_acct, &ctx, store, p, group, name_pattern, MD_FN_ACCOUNT, MD_SV_JSON);
+ if (ctx.id) {
+ *pid = ctx.id;
+ rv = md_acme_acct_load(pacct, ppkey, store, group, ctx.id, p);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_find: got account %s", ctx.id);
+ }
+ else {
+ *pacct = NULL;
+ rv = APR_ENOENT;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find: none found");
+ }
+ return rv;
+}
+
+static apr_status_t acct_find_and_verify(md_store_t *store, md_store_group_t group,
+ const char *name_pattern,
+ md_acme_t *acme, const md_t *md,
+ apr_pool_t *p)
+{
+ md_acme_acct_t *acct;
+ md_pkey_t *pkey;
+ const char *id;
+ apr_status_t rv;
+
+ rv = acct_find(&id, &acct, &pkey, store, group, name_pattern, md, p);
+ if (APR_SUCCESS == rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find_and_verify: found %s",
+ id);
+ acme->acct_id = (MD_SG_STAGING == group)? NULL : id;
+ acme->acct = acct;
+ acme->acct_key = pkey;
+ rv = md_acme_acct_validate(acme, (MD_SG_STAGING == group)? NULL : store, p);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, "acct_find_and_verify: verified %s",
+ id);
+
+ if (APR_SUCCESS != rv) {
+ acme->acct_id = NULL;
+ acme->acct = NULL;
+ acme->acct_key = NULL;
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ /* verification failed and account has been disabled.
+ Indicate to caller that he may try again. */
+ rv = APR_EAGAIN;
+ }
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_acme_find_acct_for_md(md_acme_t *acme, md_store_t *store, const md_t *md)
+{
+ apr_status_t rv;
+
+ while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_ACCOUNTS,
+ mk_acct_pattern(acme->p, acme),
+ acme, md, acme->p))) {
+ /* nop */
+ }
+
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ /* No suitable account found in MD_SG_ACCOUNTS. Maybe a new account
+ * can already be found in MD_SG_STAGING? */
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p,
+ "no account found, looking in STAGING");
+ rv = acct_find_and_verify(store, MD_SG_STAGING, "*", acme, md, acme->p);
+ if (APR_EAGAIN == rv) {
+ rv = APR_ENOENT;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store,
+ md_store_group_t group, const md_t *md,
+ apr_pool_t *p)
+{
+ apr_status_t rv;
+ find_ctx ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.p = p;
+ ctx.md = md;
+
+ rv = md_store_iter(find_acct, &ctx, store, p, group, "*", MD_FN_ACCOUNT, MD_SV_JSON);
+ if (ctx.id) {
+ *pid = ctx.id;
+ rv = APR_SUCCESS;
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_id_for_md %s -> %s", md->name, *pid);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* acct operation context */
+typedef struct {
+ md_acme_t *acme;
+ apr_pool_t *p;
+ const char *agreement;
+ const char *eab_kid;
+ const char *eab_hmac;
+} acct_ctx_t;
+
+/**************************************************************************************************/
+/* acct update */
+
+static apr_status_t on_init_acct_upd(md_acme_req_t *req, void *baton)
+{
+ (void)baton;
+ return md_acme_req_body_init(req, NULL);
+}
+
+static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p,
+ const apr_table_t *hdrs, md_json_t *body, void *baton)
+{
+ acct_ctx_t *ctx = baton;
+ apr_status_t rv = APR_SUCCESS;
+ md_acme_acct_t *acct = acme->acct;
+
+ if (md_log_is_level(p, MD_LOG_TRACE2)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, acme->p, "acct update response: %s",
+ md_json_writep(body, p, MD_JSON_FMT_COMPACT));
+ }
+
+ if (!acct->url) {
+ const char *location = apr_table_get(hdrs, "location");
+ if (!location) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, APR_EINVAL, p, "new acct without location");
+ return APR_EINVAL;
+ }
+ acct->url = apr_pstrdup(ctx->p, location);
+ }
+
+ apr_array_clear(acct->contacts);
+ md_json_dupsa(acct->contacts, acme->p, body, MD_KEY_CONTACT, NULL);
+ if (md_json_has_key(body, MD_KEY_STATUS, NULL)) {
+ acct->status = acct_st_from_str(md_json_gets(body, MD_KEY_STATUS, NULL));
+ }
+ if (md_json_has_key(body, MD_KEY_AGREEMENT, NULL)) {
+ acct->agreement = md_json_dups(acme->p, body, MD_KEY_AGREEMENT, NULL);
+ }
+ if (md_json_has_key(body, MD_KEY_ORDERS, NULL)) {
+ acct->orders = md_json_dups(acme->p, body, MD_KEY_ORDERS, NULL);
+ }
+ if (ctx->eab_kid && ctx->eab_hmac) {
+ acct->eab_kid = ctx->eab_kid;
+ acct->eab_hmac = ctx->eab_hmac;
+ }
+ acct->registration = md_json_clone(ctx->p, body);
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url);
+ return rv;
+}
+
+apr_status_t md_acme_acct_update(md_acme_t *acme)
+{
+ acct_ctx_t ctx;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct update");
+ if (!acme->acct) {
+ return APR_EINVAL;
+ }
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.acme = acme;
+ ctx.p = acme->p;
+ return md_acme_POST(acme, acme->acct->url, on_init_acct_upd, acct_upd, NULL, NULL, &ctx);
+}
+
+apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p)
+{
+ apr_status_t rv;
+
+ if (APR_SUCCESS != (rv = md_acme_acct_update(acme))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p,
+ "acct update failed for %s", acme->acct->url);
+ if (APR_EINVAL == rv && (acme->acct->agreement || !acme->ca_agreement)) {
+ /* Sadly, some proprietary ACME servers choke on empty POSTs
+ * on accounts. Try a faked ToS agreement. */
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p,
+ "trying acct update via ToS agreement");
+ rv = md_acme_agree(acme, p, "accepted");
+ }
+ if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv || APR_EINVAL == rv)) {
+ if (MD_ACME_ACCT_ST_VALID == acme->acct->status) {
+ acme->acct->status = MD_ACME_ACCT_ST_UNKNOWN;
+ if (store) {
+ md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key);
+ }
+ }
+ acme->acct = NULL;
+ acme->acct_key = NULL;
+ rv = APR_ENOENT;
+ }
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* Register a new account */
+
+static apr_status_t get_eab(md_json_t **peab, md_acme_req_t *req, const char *kid,
+ const char *hmac64, md_pkey_t *account_key,
+ const char *url)
+{
+ md_json_t *eab, *prot_fields, *jwk;
+ md_data_t payload, hmac_key;
+ apr_status_t rv;
+
+ prot_fields = md_json_create(req->p);
+ md_json_sets(url, prot_fields, "url", NULL);
+ md_json_sets(kid, prot_fields, "kid", NULL);
+
+ rv = md_jws_get_jwk(&jwk, req->p, account_key);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ md_data_null(&payload);
+ payload.data = md_json_writep(jwk, req->p, MD_JSON_FMT_COMPACT);
+ if (!payload.data) {
+ rv = APR_EINVAL;
+ goto cleanup;
+ }
+ payload.len = strlen(payload.data);
+
+ md_util_base64url_decode(&hmac_key, hmac64, req->p);
+ if (!hmac_key.len) {
+ rv = APR_EINVAL;
+ md_result_problem_set(req->result, rv, "apache:eab-hmac-invalid",
+ "external account binding HMAC value is not valid base64", NULL);
+ goto cleanup;
+ }
+
+ rv = md_jws_hmac(&eab, req->p, &payload, prot_fields, &hmac_key);
+ if (APR_SUCCESS != rv) {
+ md_result_problem_set(req->result, rv, "apache:eab-hmac-fail",
+ "external account binding MAC could not be computed", NULL);
+ }
+
+cleanup:
+ *peab = (APR_SUCCESS == rv)? eab : NULL;
+ return rv;
+}
+
+static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton)
+{
+ acct_ctx_t *ctx = baton;
+ md_json_t *jpayload, *jeab;
+ apr_status_t rv;
+
+ jpayload = md_json_create(req->p);
+ md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL);
+ if (ctx->agreement) {
+ md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL);
+ }
+ if (ctx->eab_kid && ctx->eab_hmac) {
+ rv = get_eab(&jeab, req, ctx->eab_kid, ctx->eab_hmac,
+ req->acme->acct_key, req->url);
+ if (APR_SUCCESS != rv) goto cleanup;
+ md_json_setj(jeab, jpayload, "externalAccountBinding", NULL);
+ }
+ rv = md_acme_req_body_init(req, jpayload);
+
+cleanup:
+ return rv;
+}
+
+apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store,
+ const md_t *md, apr_pool_t *p)
+{
+ apr_status_t rv;
+ md_pkey_t *pkey;
+ const char *err = NULL, *uri;
+ md_pkey_spec_t spec;
+ int i;
+ acct_ctx_t ctx;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account");
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.acme = acme;
+ ctx.p = p;
+ /* The agreement URL is submitted when the ACME server announces Terms-of-Service
+ * in its directory meta data. The magic value "accepted" will always use the
+ * advertised URL. */
+ ctx.agreement = NULL;
+ if (acme->ca_agreement && md->ca_agreement) {
+ ctx.agreement = !strcmp("accepted", md->ca_agreement)?
+ acme->ca_agreement : md->ca_agreement;
+ }
+
+ if (ctx.agreement) {
+ if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, ctx.agreement, &err))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "invalid agreement uri (%s): %s", err, ctx.agreement);
+ goto out;
+ }
+ }
+ ctx.eab_kid = md->ca_eab_kid;
+ ctx.eab_hmac = md->ca_eab_hmac;
+
+ for (i = 0; i < md->contacts->nelts; ++i) {
+ uri = APR_ARRAY_IDX(md->contacts, i, const char *);
+ if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "invalid contact uri (%s): %s", err, uri);
+ goto out;
+ }
+ }
+
+ /* If there is no key selected yet, try to find an existing one for the same host.
+ * Let's Encrypt identifies accounts by their key for their ACMEv1 and v2 services.
+ * Although the account appears on both services with different urls, it is
+ * internally the same one.
+ * I think this is beneficial if someone migrates from ACMEv1 to v2 and not a leak
+ * of identifying information.
+ */
+ if (!acme->acct_key) {
+ find_ctx fctx;
+
+ memset(&fctx, 0, sizeof(fctx));
+ fctx.p = p;
+ fctx.md = md;
+
+ md_store_iter(find_acct, &fctx, store, p, MD_SG_ACCOUNTS,
+ mk_acct_pattern(p, acme), MD_FN_ACCOUNT, MD_SV_JSON);
+ if (fctx.id) {
+ rv = md_store_load(store, MD_SG_ACCOUNTS, fctx.id, MD_FN_ACCT_KEY, MD_SV_PKEY,
+ (void**)&acme->acct_key, p);
+ if (APR_SUCCESS == rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "reusing key from account %s", fctx.id);
+ }
+ else {
+ acme->acct_key = NULL;
+ }
+ }
+ }
+
+ /* If we still have no key, generate a new one */
+ if (!acme->acct_key) {
+ spec.type = MD_PKEY_TYPE_RSA;
+ spec.params.rsa.bits = MD_ACME_ACCT_PKEY_BITS;
+
+ if (APR_SUCCESS != (rv = md_pkey_gen(&pkey, acme->p, &spec))) goto out;
+ acme->acct_key = pkey;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "created new account key");
+ }
+
+ if (APR_SUCCESS != (rv = acct_make(&acme->acct, p, acme->url, md->contacts))) goto out;
+ rv = md_acme_POST_new_account(acme, on_init_acct_new, acct_upd, NULL, NULL, &ctx);
+ if (APR_SUCCESS == rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p,
+ "registered new account %s", acme->acct->url);
+ }
+
+out:
+ if (APR_SUCCESS != rv && acme->acct) {
+ acme->acct = NULL;
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* Deactivate the account */
+
+static apr_status_t on_init_acct_del(md_acme_req_t *req, void *baton)
+{
+ md_json_t *jpayload;
+
+ (void)baton;
+ jpayload = md_json_create(req->p);
+ md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL);
+ return md_acme_req_body_init(req, jpayload);
+}
+
+apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p)
+{
+ md_acme_acct_t *acct = acme->acct;
+ acct_ctx_t ctx;
+
+ (void)p;
+ if (!acct) {
+ return APR_EINVAL;
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s",
+ acct->url, acct->ca_url);
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.acme = acme;
+ ctx.p = p;
+ return md_acme_POST(acme, acct->url, on_init_acct_del, acct_upd, NULL, NULL, &ctx);
+}
+
+/**************************************************************************************************/
+/* terms-of-service */
+
+static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *baton)
+{
+ acct_ctx_t *ctx = baton;
+ md_json_t *jpayload;
+
+ jpayload = md_json_create(req->p);
+ if (ctx->acme->acct->agreement) {
+ md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL);
+ }
+ return md_acme_req_body_init(req, jpayload);
+}
+
+apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *agreement)
+{
+ acct_ctx_t ctx;
+
+ acme->acct->agreement = agreement;
+ if (!strcmp("accepted", agreement) && acme->ca_agreement) {
+ acme->acct->agreement = acme->ca_agreement;
+ }
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.acme = acme;
+ ctx.p = p;
+ return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, NULL, &ctx);
+}
+
+apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p,
+ const char *agreement, const char **prequired)
+{
+ apr_status_t rv = APR_SUCCESS;
+
+ /* We used to really check if the account agreement and the one indicated in meta
+ * are the very same. However, LE is happy if the account has agreed to a ToS in
+ * the past and does not require a renewed acceptance.
+ */
+ *prequired = NULL;
+ if (!acme->acct->agreement && acme->ca_agreement) {
+ if (agreement) {
+ rv = md_acme_agree(acme, p, acme->ca_agreement);
+ }
+ else {
+ *prequired = acme->ca_agreement;
+ rv = APR_INCOMPLETE;
+ }
+ }
+ return rv;
+}
diff --git a/modules/md/md_acme_acct.h b/modules/md/md_acme_acct.h
new file mode 100644
index 0000000..b5bba63
--- /dev/null
+++ b/modules/md/md_acme_acct.h
@@ -0,0 +1,148 @@
+/* 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_acme_acct_h
+#define mod_md_md_acme_acct_h
+
+struct md_acme_req;
+struct md_json_t;
+struct md_pkey_t;
+
+#include "md_store.h"
+
+/**
+ * An ACME account at an ACME server.
+ */
+typedef struct md_acme_acct_t md_acme_acct_t;
+
+typedef enum {
+ MD_ACME_ACCT_ST_UNKNOWN,
+ MD_ACME_ACCT_ST_VALID,
+ MD_ACME_ACCT_ST_DEACTIVATED,
+ MD_ACME_ACCT_ST_REVOKED,
+} md_acme_acct_st;
+
+struct md_acme_acct_t {
+ const char *id; /* short, unique id for the account */
+ const char *url; /* url of the account, once registered */
+ const char *ca_url; /* url of the ACME protocol endpoint */
+ md_acme_acct_st status; /* status of this account */
+ apr_array_header_t *contacts; /* list of contact uris, e.g. mailto:xxx */
+ const char *tos_required; /* terms of service asked for by CA */
+ const char *agreement; /* terms of service agreed to by user */
+ const char *orders; /* URL where certificate orders are found (ACMEv2) */
+ const char *eab_kid; /* external account binding keyid used or NULL */
+ const char *eab_hmac; /* external account binding hmac used or NULL */
+ struct md_json_t *registration; /* data from server registration */
+};
+
+#define MD_FN_ACCOUNT "account.json"
+#define MD_FN_ACCT_KEY "account.pem"
+
+/* ACME account private keys are always RSA and have that many bits. Since accounts
+ * are expected to live long, better err on the safe side. */
+#define MD_ACME_ACCT_PKEY_BITS 3072
+
+#define MD_ACME_ACCT_STAGED "staged"
+
+/**
+ * Convert an ACME account form/to JSON.
+ */
+struct md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p);
+apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, struct md_json_t *json, apr_pool_t *p);
+
+/**
+ * Update the account from the ACME server.
+ * - Will update acme->acct structure from server on success
+ * - Will return error status when request failed or account is not known.
+ */
+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, md_store_t *store, apr_pool_t *p);
+
+/**
+ * Agree to the given Terms-of-Service url for the current account.
+ */
+apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *tos);
+
+/**
+ * Confirm with the server that the current account agrees to the Terms-of-Service
+ * given in the agreement url.
+ * If the known agreement is equal to this, nothing is done.
+ * If it differs, the account is re-validated in the hope that the server
+ * announces the Tos URL it wants. If this is equal to the agreement specified,
+ * the server is notified of this. If the server requires a ToS that the account
+ * thinks it has already given, it is resend.
+ *
+ * If an agreement is required, different from the current one, APR_INCOMPLETE is
+ * returned and the agreement url is returned in the parameter.
+ */
+apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p,
+ const char *agreement, const char **prequired);
+
+/**
+ * Get the ToS agreement for current account.
+ */
+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_for_md(md_acme_t *acme, md_store_t *store, const md_t *md);
+
+/**
+ * Find the account id for a given md.
+ */
+apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store,
+ md_store_group_t group, const md_t *md, apr_pool_t *p);
+
+/**
+ * Create a new account at the ACME server for an MD. 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, md_store_t *store,
+ const md_t *md, apr_pool_t *p);
+
+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);
+
+/**
+ * Deactivate the current account at the ACME server.
+ */
+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,
+ md_store_t *store, md_store_group_t group,
+ const char *name, apr_pool_t *p);
+
+/*
+ * Return != 0 iff the account can be used for the ACME url.
+ */
+int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url);
+
+/*
+ * Return != 0 iff the account can be used for the MD, including
+ * its CA url and EAB settings.
+ */
+int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md);
+
+#endif /* md_acme_acct_h */
diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c
new file mode 100644
index 0000000..a55804e
--- /dev/null
+++ b/modules/md/md_acme_authz.c
@@ -0,0 +1,697 @@
+/* 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 <apr_lib.h>
+#include <apr_buckets.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_jws.h"
+#include "md_result.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_authz.h"
+
+md_acme_authz_t *md_acme_authz_create(apr_pool_t *p)
+{
+ md_acme_authz_t *authz;
+ authz = apr_pcalloc(p, sizeof(*authz));
+
+ return authz;
+}
+
+/**************************************************************************************************/
+/* Register a new authorization */
+
+typedef struct {
+ size_t index;
+ const char *type;
+ const char *uri;
+ const char *token;
+ const char *key_authz;
+} md_acme_authz_cha_t;
+
+typedef struct {
+ apr_pool_t *p;
+ md_acme_t *acme;
+ const char *domain;
+ md_acme_authz_t *authz;
+ md_acme_authz_cha_t *challenge;
+} authz_req_ctx;
+
+static void authz_req_ctx_init(authz_req_ctx *ctx, md_acme_t *acme,
+ const char *domain, md_acme_authz_t *authz, apr_pool_t *p)
+{
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->p = p;
+ ctx->acme = acme;
+ ctx->domain = domain;
+ ctx->authz = authz;
+}
+
+/**************************************************************************************************/
+/* Update an existing authorization */
+
+apr_status_t md_acme_authz_retrieve(md_acme_t *acme, apr_pool_t *p, const char *url,
+ md_acme_authz_t **pauthz)
+{
+ md_acme_authz_t *authz;
+ apr_status_t rv;
+
+ authz = apr_pcalloc(p, sizeof(*authz));
+ authz->url = apr_pstrdup(p, url);
+ rv = md_acme_authz_update(authz, acme, p);
+
+ *pauthz = (APR_SUCCESS == rv)? authz : NULL;
+ 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);
+ assert(authz);
+ assert(authz->url);
+
+ 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;
+
+ if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, p))
+ && (s = md_json_gets(json, MD_KEY_STATUS, NULL))) {
+
+ authz->domain = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL);
+ authz->resource = json;
+ if (!strcmp(s, "pending")) {
+ authz->state = MD_ACME_AUTHZ_S_PENDING;
+ err = "challenge 'pending'";
+ log_level = MD_LOG_DEBUG;
+ }
+ else if (!strcmp(s, "valid")) {
+ authz->state = MD_ACME_AUTHZ_S_VALID;
+ err = "challenge 'valid'";
+ 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'";
+ }
+ }
+
+ if (json && authz->state == MD_ACME_AUTHZ_S_UNKNOWN) {
+ err = "unable to understand response";
+ rv = APR_EINVAL;
+ }
+
+ 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, authz->domain, authz->url,
+ json? md_json_writep(json, p, MD_JSON_FMT_COMPACT) : "not available");
+ }
+
+ return rv;
+}
+
+/**************************************************************************************************/
+/* response to a challenge */
+
+static md_acme_authz_cha_t *cha_from_json(apr_pool_t *p, size_t index, md_json_t *json)
+{
+ md_acme_authz_cha_t * cha;
+
+ cha = apr_pcalloc(p, sizeof(*cha));
+ cha->index = index;
+ cha->type = md_json_dups(p, json, MD_KEY_TYPE, NULL);
+ if (md_json_has_key(json, MD_KEY_URL, NULL)) { /* ACMEv2 */
+ cha->uri = md_json_dups(p, json, MD_KEY_URL, NULL);
+ }
+ else { /* ACMEv1 */
+ cha->uri = md_json_dups(p, json, MD_KEY_URI, NULL);
+ }
+ cha->token = md_json_dups(p, json, MD_KEY_TOKEN, NULL);
+ cha->key_authz = md_json_dups(p, json, MD_KEY_KEYAUTHZ, NULL);
+
+ return cha;
+}
+
+static apr_status_t on_init_authz_resp(md_acme_req_t *req, void *baton)
+{
+ md_json_t *jpayload;
+
+ (void)baton;
+ jpayload = md_json_create(req->p);
+ return md_acme_req_body_init(req, jpayload);
+}
+
+static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs,
+ md_json_t *body, void *baton)
+{
+ authz_req_ctx *ctx = baton;
+
+ (void)acme;
+ (void)p;
+ (void)hdrs;
+ (void)body;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ctx->p, "updated authz %s", ctx->authz->url);
+ return APR_SUCCESS;
+}
+
+static apr_status_t setup_key_authz(md_acme_authz_cha_t *cha, md_acme_authz_t *authz,
+ md_acme_t *acme, apr_pool_t *p, int *pchanged)
+{
+ const char *thumb64, *key_authz;
+ apr_status_t rv;
+
+ (void)authz;
+ assert(cha);
+ assert(cha->token);
+
+ *pchanged = 0;
+ if (APR_SUCCESS == (rv = md_jws_pkey_thumb(&thumb64, p, acme->acct_key))) {
+ key_authz = apr_psprintf(p, "%s.%s", cha->token, thumb64);
+ if (cha->key_authz) {
+ if (strcmp(key_authz, cha->key_authz)) {
+ /* Hu? Did the account change key? */
+ cha->key_authz = NULL;
+ }
+ }
+ if (!cha->key_authz) {
+ cha->key_authz = key_authz;
+ *pchanged = 1;
+ }
+ }
+ return rv;
+}
+
+static apr_status_t cha_http_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz,
+ md_acme_t *acme, md_store_t *store,
+ md_pkeys_spec_t *key_specs,
+ apr_array_header_t *acme_tls_1_domains, const md_t *md,
+ apr_table_t *env, md_result_t *result, apr_pool_t *p)
+{
+ const char *data;
+ apr_status_t rv;
+ int notify_server;
+
+ (void)key_specs;
+ (void)env;
+ (void)acme_tls_1_domains;
+ (void)md;
+
+ if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, &notify_server))) {
+ goto out;
+ }
+
+ rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01,
+ MD_SV_TEXT, (void**)&data, p);
+ if ((APR_SUCCESS == rv && strcmp(cha->key_authz, data)) || APR_STATUS_IS_ENOENT(rv)) {
+ const char *content = apr_psprintf(p, "%s\n", cha->key_authz);
+ rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01,
+ MD_SV_TEXT, (void*)content, 0);
+ notify_server = 1;
+ }
+
+ if (APR_SUCCESS == rv && notify_server) {
+ authz_req_ctx ctx;
+ const char *event;
+
+ /* Raise event that challenge data has been set up before we tell the
+ ACME server. Clusters might want to distribute it. */
+ event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_HTTP01, authz->domain);
+ rv = md_result_raise(result, event, p);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "%s: event '%s' failed. aborting challenge setup",
+ authz->domain, event);
+ goto out;
+ }
+ /* challenge is setup or was changed from previous data, tell ACME server
+ * so it may (re)try verification */
+ authz_req_ctx_init(&ctx, acme, NULL, authz, p);
+ ctx.challenge = cha;
+ rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx);
+ }
+out:
+ return rv;
+}
+
+void tls_alpn01_fnames(apr_pool_t *p, md_pkey_spec_t *kspec, char **keyfn, char **certfn )
+{
+ *keyfn = apr_pstrcat(p, "acme-tls-alpn-01-", md_pkey_filename(kspec, p), NULL);
+ *certfn = apr_pstrcat(p, "acme-tls-alpn-01-", md_chain_filename(kspec, p), NULL);
+}
+
+static apr_status_t cha_tls_alpn_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz,
+ md_acme_t *acme, md_store_t *store,
+ md_pkeys_spec_t *key_specs,
+ apr_array_header_t *acme_tls_1_domains, const md_t *md,
+ apr_table_t *env, md_result_t *result, apr_pool_t *p)
+{
+ const char *acme_id, *token;
+ apr_status_t rv;
+ int notify_server;
+ md_data_t data;
+ int i;
+
+ (void)env;
+ (void)md;
+ if (md_array_str_index(acme_tls_1_domains, authz->domain, 0, 0) < 0) {
+ rv = APR_ENOTIMPL;
+ if (acme_tls_1_domains->nelts) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p,
+ "%s: protocol 'acme-tls/1' seems not enabled for this domain, "
+ "but is enabled for other associated domains. "
+ "Continuing with fingers crossed.", authz->domain);
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p,
+ "%s: protocol 'acme-tls/1' seems not enabled for this or "
+ "any other associated domain. Not attempting challenge "
+ "type tls-alpn-01.", authz->domain);
+ goto out;
+ }
+ }
+ if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, &notify_server))) {
+ goto out;
+ }
+
+ /* Create a "tls-alpn-01" certificate for the domain we want to authenticate.
+ * The server will need to answer a TLS connection with SNI == authz->domain
+ * and ALPN protocol "acme-tls/1" with this certificate.
+ */
+ md_data_init_str(&data, cha->key_authz);
+ rv = md_crypt_sha256_digest_hex(&token, p, &data);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 validation token",
+ authz->domain);
+ goto out;
+ }
+ acme_id = apr_psprintf(p, "critical,DER:04:20:%s", token);
+
+ /* Each configured key type must be generated to ensure:
+ * that any fallback certs already given to mod_ssl are replaced.
+ * We expect that the validation client (at the CA) can deal with at
+ * least one of them.
+ */
+
+ for (i = 0; i < md_pkeys_spec_count(key_specs); ++i) {
+ char *kfn, *cfn;
+ md_cert_t *cha_cert;
+ md_pkey_t *cha_key;
+ md_pkey_spec_t *key_spec;
+
+ key_spec = md_pkeys_spec_get(key_specs, i);
+ tls_alpn01_fnames(p, key_spec, &kfn, &cfn);
+
+ rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, cfn,
+ MD_SV_CERT, (void**)&cha_cert, p);
+ if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, authz->domain))
+ || APR_STATUS_IS_ENOENT(rv)) {
+ if (APR_SUCCESS != (rv = md_pkey_gen(&cha_key, p, key_spec))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 %s challenge key",
+ authz->domain, md_pkey_spec_name(key_spec));
+ goto out;
+ }
+
+ if (APR_SUCCESS != (rv = md_cert_make_tls_alpn_01(&cha_cert, authz->domain, acme_id, cha_key,
+ apr_time_from_sec(7 * MD_SECS_PER_DAY), p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 %s challenge cert",
+ authz->domain, md_pkey_spec_name(key_spec));
+ goto out;
+ }
+
+ if (APR_SUCCESS == (rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, kfn,
+ MD_SV_PKEY, (void*)cha_key, 0))) {
+ rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, cfn,
+ MD_SV_CERT, (void*)cha_cert, 0);
+ }
+ ++notify_server;
+ }
+ }
+
+ if (APR_SUCCESS == rv && notify_server) {
+ authz_req_ctx ctx;
+ const char *event;
+
+ /* Raise event that challenge data has been set up before we tell the
+ ACME server. Clusters might want to distribute it. */
+ event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_TLSALPN01, authz->domain);
+ rv = md_result_raise(result, event, p);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "%s: event '%s' failed. aborting challenge setup",
+ authz->domain, event);
+ goto out;
+ }
+ /* challenge is setup or was changed from previous data, tell ACME server
+ * so it may (re)try verification */
+ authz_req_ctx_init(&ctx, acme, NULL, authz, p);
+ ctx.challenge = cha;
+ rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx);
+ }
+out:
+ return rv;
+}
+
+static apr_status_t cha_dns_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz,
+ md_acme_t *acme, md_store_t *store,
+ md_pkeys_spec_t *key_specs,
+ apr_array_header_t *acme_tls_1_domains, const md_t *md,
+ apr_table_t *env, md_result_t *result, apr_pool_t *p)
+{
+ const char *token;
+ const char * const *argv;
+ const char *cmdline, *dns01_cmd;
+ apr_status_t rv;
+ int exit_code, notify_server;
+ authz_req_ctx ctx;
+ md_data_t data;
+ const char *event;
+
+ (void)store;
+ (void)key_specs;
+ (void)acme_tls_1_domains;
+
+ dns01_cmd = md->dns01_cmd;
+ if (!dns01_cmd)
+ dns01_cmd = apr_table_get(env, MD_KEY_CMD_DNS01);
+ if (!dns01_cmd) {
+ rv = APR_ENOTIMPL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 command not set",
+ authz->domain);
+ goto out;
+ }
+
+ if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, &notify_server))) {
+ goto out;
+ }
+
+ md_data_init_str(&data, cha->key_authz);
+ rv = md_crypt_sha256_digest64(&token, p, &data);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create dns-01 token for %s",
+ md->name, authz->domain);
+ goto out;
+ }
+
+ cmdline = apr_psprintf(p, "%s setup %s %s", dns01_cmd, authz->domain, token);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p,
+ "%s: dns-01 setup command: %s", authz->domain, cmdline);
+
+ apr_tokenize_to_argv(cmdline, (char***)&argv, p);
+ if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, NULL, &exit_code))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p,
+ "%s: dns-01 setup command failed to execute for %s", md->name, authz->domain);
+ goto out;
+ }
+ if (exit_code) {
+ rv = APR_EGENERAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p,
+ "%s: dns-01 setup command returns %d for %s", md->name, exit_code, authz->domain);
+ goto out;
+ }
+
+ /* Raise event that challenge data has been set up before we tell the
+ ACME server. Clusters might want to distribute it. */
+ event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_DNS01, authz->domain);
+ rv = md_result_raise(result, event, p);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "%s: event '%s' failed. aborting challenge setup",
+ authz->domain, event);
+ goto out;
+ }
+ /* challenge is setup, tell ACME server so it may (re)try verification */
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 setup succeeded for %s",
+ md->name, authz->domain);
+ authz_req_ctx_init(&ctx, acme, NULL, authz, p);
+ ctx.challenge = cha;
+ rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx);
+
+out:
+ return rv;
+}
+
+static apr_status_t cha_dns_01_teardown(md_store_t *store, const char *domain, const md_t *md,
+ apr_table_t *env, apr_pool_t *p)
+{
+ const char * const *argv;
+ const char *cmdline, *dns01_cmd;
+ apr_status_t rv;
+ int exit_code;
+
+ (void)store;
+
+ dns01_cmd = md->dns01_cmd;
+ if (!dns01_cmd)
+ dns01_cmd = apr_table_get(env, MD_KEY_CMD_DNS01);
+ if (!dns01_cmd) {
+ rv = APR_ENOTIMPL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: dns-01 command not set for %s",
+ md->name, domain);
+ goto out;
+ }
+
+ cmdline = apr_psprintf(p, "%s teardown %s", dns01_cmd, domain);
+ apr_tokenize_to_argv(cmdline, (char***)&argv, p);
+ if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, NULL, &exit_code)) || exit_code) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p,
+ "%s: dns-01 teardown command failed (exit code=%d) for %s",
+ md->name, exit_code, domain);
+ }
+out:
+ return rv;
+}
+
+static apr_status_t cha_teardown_dir(md_store_t *store, const char *domain, const md_t *md,
+ apr_table_t *env, apr_pool_t *p)
+{
+ (void)md;
+ (void)env;
+ return md_store_purge(store, p, MD_SG_CHALLENGES, domain);
+}
+
+typedef apr_status_t cha_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz,
+ md_acme_t *acme, md_store_t *store,
+ md_pkeys_spec_t *key_specs,
+ apr_array_header_t *acme_tls_1_domains, const md_t *md,
+ apr_table_t *env, md_result_t *result, apr_pool_t *p);
+
+typedef apr_status_t cha_teardown(md_store_t *store, const char *domain, const md_t *md,
+ apr_table_t *env, apr_pool_t *p);
+
+typedef struct {
+ const char *name;
+ cha_setup *setup;
+ cha_teardown *teardown;
+} cha_type;
+
+static const cha_type CHA_TYPES[] = {
+ { MD_AUTHZ_TYPE_HTTP01, cha_http_01_setup, cha_teardown_dir },
+ { MD_AUTHZ_TYPE_TLSALPN01, cha_tls_alpn_01_setup, cha_teardown_dir },
+ { MD_AUTHZ_TYPE_DNS01, cha_dns_01_setup, cha_dns_01_teardown },
+};
+static const apr_size_t CHA_TYPES_LEN = (sizeof(CHA_TYPES)/sizeof(CHA_TYPES[0]));
+
+typedef struct {
+ apr_pool_t *p;
+ const char *type;
+ md_acme_authz_cha_t *accepted;
+ apr_array_header_t *offered;
+} cha_find_ctx;
+
+static apr_status_t collect_offered(void *baton, size_t index, md_json_t *json)
+{
+ cha_find_ctx *ctx = baton;
+ const char *ctype;
+
+ (void)index;
+ if ((ctype = md_json_gets(json, MD_KEY_TYPE, NULL))) {
+ APR_ARRAY_PUSH(ctx->offered, const char*) = apr_pstrdup(ctx->p, ctype);
+ }
+ return 1;
+}
+
+static apr_status_t find_type(void *baton, size_t index, md_json_t *json)
+{
+ cha_find_ctx *ctx = baton;
+
+ const char *ctype = md_json_gets(json, MD_KEY_TYPE, NULL);
+ if (ctype && !apr_strnatcasecmp(ctx->type, ctype)) {
+ ctx->accepted = cha_from_json(ctx->p, index, json);
+ return 0;
+ }
+ return 1;
+}
+
+apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store,
+ apr_array_header_t *challenges, md_pkeys_spec_t *key_specs,
+ apr_array_header_t *acme_tls_1_domains, const md_t *md,
+ apr_table_t *env, apr_pool_t *p, const char **psetup_token,
+ md_result_t *result)
+{
+ apr_status_t rv;
+ int i, j;
+ cha_find_ctx fctx;
+ const char *challenge_setup;
+
+ assert(acme);
+ assert(authz);
+ assert(authz->resource);
+
+ fctx.p = p;
+ fctx.accepted = NULL;
+
+ /* Look in the order challenge types are defined:
+ * - if they are offered by the CA, try to set it up
+ * - if setup was successful, we are done and the CA will evaluate us
+ * - if setup failed, continue to look for another supported challenge type
+ * - if there is no overlap in types, tell the user that she has to configure
+ * either more types (dns, tls-alpn-01), make ports available or refrain
+ * from using wildcard domains when dns is not available. etc.
+ * - if there was an overlap, but no setup was successful, report that. We
+ * will retry this, maybe the failure is temporary (e.g. command to setup DNS
+ */
+ md_result_printf(result, 0, "%s: selecting suitable authorization challenge "
+ "type, this domain supports %s",
+ authz->domain, apr_array_pstrcat(p, challenges, ' '));
+ rv = APR_ENOTIMPL;
+ challenge_setup = NULL;
+ for (i = 0; i < challenges->nelts; ++i) {
+ fctx.type = APR_ARRAY_IDX(challenges, i, const char *);
+ fctx.accepted = NULL;
+ md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p,
+ "%s: challenge type '%s' for %s: %s",
+ authz->domain, fctx.type, md->name,
+ fctx.accepted? "maybe acceptable" : "not applicable");
+
+ if (fctx.accepted) {
+ for (j = 0; j < (int)CHA_TYPES_LEN; ++j) {
+ if (!apr_strnatcasecmp(CHA_TYPES[j].name, fctx.accepted->type)) {
+ md_result_activity_printf(result, "Setting up challenge '%s' for domain %s",
+ fctx.accepted->type, authz->domain);
+ rv = CHA_TYPES[j].setup(fctx.accepted, authz, acme, store, key_specs,
+ acme_tls_1_domains, md, env, result, p);
+ if (APR_SUCCESS == rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "%s: set up challenge '%s' for %s",
+ authz->domain, fctx.accepted->type, md->name);
+ challenge_setup = CHA_TYPES[j].name;
+ goto out;
+ }
+ md_result_printf(result, rv, "error setting up challenge '%s' for %s, "
+ "for domain %s, looking for other option",
+ fctx.accepted->type, authz->domain, md->name);
+ md_result_log(result, MD_LOG_INFO);
+ }
+ }
+ }
+ }
+
+out:
+ *psetup_token = (APR_SUCCESS == rv)? apr_psprintf(p, "%s:%s", challenge_setup, authz->domain) : NULL;
+ if (!fctx.accepted || APR_ENOTIMPL == rv) {
+ rv = APR_EINVAL;
+ fctx.offered = apr_array_make(p, 5, sizeof(const char*));
+ md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL);
+ md_result_printf(result, rv, "None of offered challenge types for domain %s are supported. "
+ "The server offered '%s' and available are: '%s'.",
+ authz->domain,
+ apr_array_pstrcat(p, fctx.offered, ' '),
+ apr_array_pstrcat(p, challenges, ' '));
+ result->problem = "challenge-mismatch";
+ md_result_log(result, MD_LOG_ERR);
+ }
+ else if (APR_SUCCESS != rv) {
+ fctx.offered = apr_array_make(p, 5, sizeof(const char*));
+ md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL);
+ md_result_printf(result, rv, "None of the offered challenge types %s offered "
+ "for domain %s could be setup successfully. Please check the "
+ "log for errors.", authz->domain,
+ apr_array_pstrcat(p, fctx.offered, ' '));
+ result->problem = "challenge-setup-failure";
+ md_result_log(result, MD_LOG_ERR);
+ }
+ return rv;
+}
+
+apr_status_t md_acme_authz_teardown(struct md_store_t *store, const char *token,
+ const md_t *md, apr_table_t *env, apr_pool_t *p)
+{
+ char *challenge, *domain;
+ int i;
+
+ if (strchr(token, ':')) {
+ challenge = apr_pstrdup(p, token);
+ domain = strchr(challenge, ':');
+ *domain = '\0'; domain++;
+ for (i = 0; i < (int)CHA_TYPES_LEN; ++i) {
+ if (!apr_strnatcasecmp(CHA_TYPES[i].name, challenge)) {
+ if (CHA_TYPES[i].teardown) {
+ return CHA_TYPES[i].teardown(store, domain, md, env, p);
+ }
+ break;
+ }
+ }
+ }
+ return APR_SUCCESS;
+}
+
diff --git a/modules/md/md_acme_authz.h b/modules/md/md_acme_authz.h
new file mode 100644
index 0000000..d74beeb
--- /dev/null
+++ b/modules/md/md_acme_authz.h
@@ -0,0 +1,79 @@
+/* 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_acme_authz_h
+#define mod_md_md_acme_authz_h
+
+struct apr_array_header_t;
+struct apr_table_t;
+struct md_acme_t;
+struct md_acme_acct_t;
+struct md_json_t;
+struct md_store_t;
+struct md_pkey_spec_t;
+struct md_result_t;
+
+typedef struct md_acme_challenge_t md_acme_challenge_t;
+
+/**************************************************************************************************/
+/* authorization request for a specific domain name */
+
+#define MD_AUTHZ_TYPE_DNS01 "dns-01"
+#define MD_AUTHZ_TYPE_HTTP01 "http-01"
+#define MD_AUTHZ_TYPE_TLSALPN01 "tls-alpn-01"
+
+typedef enum {
+ MD_ACME_AUTHZ_S_UNKNOWN,
+ MD_ACME_AUTHZ_S_PENDING,
+ MD_ACME_AUTHZ_S_VALID,
+ MD_ACME_AUTHZ_S_INVALID,
+} md_acme_authz_state_t;
+
+typedef struct md_acme_authz_t md_acme_authz_t;
+
+struct md_acme_authz_t {
+ const char *domain;
+ 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;
+};
+
+#define MD_FN_HTTP01 "acme-http-01.txt"
+
+void tls_alpn01_fnames(apr_pool_t *p, struct md_pkey_spec_t *kspec, char **keyfn, char **certfn );
+
+md_acme_authz_t *md_acme_authz_create(apr_pool_t *p);
+
+apr_status_t md_acme_authz_retrieve(md_acme_t *acme, apr_pool_t *p, const char *url,
+ md_acme_authz_t **pauthz);
+apr_status_t md_acme_authz_update(md_acme_authz_t *authz, struct md_acme_t *acme, apr_pool_t *p);
+
+apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, struct md_acme_t *acme,
+ struct md_store_t *store, apr_array_header_t *challenges,
+ struct md_pkeys_spec_t *key_spec,
+ apr_array_header_t *acme_tls_1_domains, const md_t *md,
+ struct apr_table_t *env,
+ apr_pool_t *p, const char **setup_token,
+ struct md_result_t *result);
+
+apr_status_t md_acme_authz_teardown(struct md_store_t *store, const char *setup_token,
+ const md_t *md, struct apr_table_t *env, apr_pool_t *p);
+
+#endif /* md_acme_authz_h */
diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c
new file mode 100644
index 0000000..4bb04f3
--- /dev/null
+++ b/modules/md/md_acme_drive.c
@@ -0,0 +1,1106 @@
+/* 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 <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+#include "md_acme_authz.h"
+#include "md_acme_order.h"
+
+#include "md_acme_drive.h"
+#include "md_acmev2_drive.h"
+
+/**************************************************************************************************/
+/* account setup */
+
+static apr_status_t use_staged_acct(md_acme_t *acme, struct md_store_t *store,
+ const md_t *md, apr_pool_t *p)
+{
+ md_acme_acct_t *acct;
+ md_pkey_t *pkey;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, store,
+ MD_SG_STAGING, md->name, acme->p))) {
+ acme->acct_id = NULL;
+ acme->acct = acct;
+ acme->acct_key = pkey;
+ rv = md_acme_acct_validate(acme, NULL, p);
+ }
+ return rv;
+}
+
+static apr_status_t save_acct_staged(md_acme_t *acme, md_store_t *store,
+ const char *md_name, apr_pool_t *p)
+{
+ md_json_t *jacct;
+ apr_status_t rv;
+
+ jacct = md_acme_acct_to_json(acme->acct, p);
+
+ rv = md_store_save(store, p, MD_SG_STAGING, md_name, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0);
+ if (APR_SUCCESS == rv) {
+ rv = md_store_save(store, p, MD_SG_STAGING, md_name, MD_FN_ACCT_KEY,
+ MD_SV_PKEY, acme->acct_key, 0);
+ }
+ return rv;
+}
+
+apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result)
+{
+ md_acme_driver_t *ad = d->baton;
+ md_t *md = ad->md;
+ apr_status_t rv = APR_SUCCESS;
+ int update_md = 0, update_acct = 0;
+
+ md_result_activity_printf(result, "Selecting account to use for %s", d->md->name);
+ md_acme_clear_acct(ad->acme);
+
+ /* Do we have a staged (modified) account? */
+ if (APR_SUCCESS == (rv = use_staged_acct(ad->acme, d->store, md, d->p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-using staged account");
+ }
+ else if (!APR_STATUS_IS_ENOENT(rv)) {
+ goto leave;
+ }
+
+ /* Get an account for the ACME server for this MD */
+ if (!ad->acme->acct && md->ca_account) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-use account '%s'", md->ca_account);
+ rv = md_acme_use_acct_for_md(ad->acme, d->store, d->p, md->ca_account, md);
+ if (APR_STATUS_IS_ENOENT(rv) || APR_STATUS_IS_EINVAL(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "rejected %s", md->ca_account);
+ md->ca_account = NULL;
+ update_md = 1;
+ }
+ else if (APR_SUCCESS != rv) {
+ goto leave;
+ }
+ }
+
+ if (!ad->acme->acct && !md->ca_account) {
+ /* Find a local account for server, store at MD */
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: looking at existing accounts",
+ d->proto->protocol);
+ if (APR_SUCCESS == (rv = md_acme_find_acct_for_md(ad->acme, d->store, md))) {
+ md->ca_account = md_acme_acct_id_get(ad->acme);
+ update_md = 1;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: using account %s (id=%s)",
+ d->proto->protocol, ad->acme->acct->url, md->ca_account);
+ }
+ }
+
+ if (!ad->acme->acct) {
+ /* No account staged, no suitable found in store, register a new one */
+ md_result_activity_printf(result, "Creating new ACME account for %s", d->md->name);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: creating new account",
+ d->proto->protocol);
+
+ if (!ad->md->contacts || apr_is_empty_array(md->contacts)) {
+ rv = APR_EINVAL;
+ md_result_printf(result, rv, "No contact information is available for MD %s. "
+ "Configure one using the MDContactEmail or ServerAdmin directive.", md->name);
+ md_result_log(result, MD_LOG_ERR);
+ goto leave;
+ }
+
+ /* ACMEv1 allowed registration of accounts without accepted Terms-of-Service.
+ * ACMEv2 requires it. Fail early in this case with a meaningful error message.
+ */
+ if (!md->ca_agreement) {
+ md_result_printf(result, APR_EINVAL,
+ "the CA requires you to accept the terms-of-service "
+ "as specified in <%s>. "
+ "Please read the document that you find at that URL and, "
+ "if you agree to the conditions, configure "
+ "\"MDCertificateAgreement accepted\" "
+ "in your Apache. Then (graceful) restart the server to activate.",
+ ad->acme->ca_agreement);
+ md_result_log(result, MD_LOG_ERR);
+ rv = result->status;
+ goto leave;
+ }
+
+ if (ad->acme->eab_required && (!md->ca_eab_kid || !strcmp("none", md->ca_eab_kid))) {
+ md_result_printf(result, APR_EINVAL,
+ "the CA requires 'External Account Binding' which is not "
+ "configured. This means you need to obtain a 'Key ID' and a "
+ "'HMAC' from the CA and configure that using the "
+ "MDExternalAccountBinding directive in your config. "
+ "The creation of a new ACME account will most likely fail, "
+ "but an attempt is made anyway.",
+ ad->acme->ca_agreement);
+ md_result_log(result, MD_LOG_INFO);
+ }
+
+ rv = md_acme_acct_register(ad->acme, d->store, md, d->p);
+ 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;
+ }
+
+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);
+ }
+ /* Persist account changes in STAGING, so we pick them up on next run */
+ if (APR_SUCCESS == rv && update_acct) {
+ rv = save_acct_staged(ad->acme, d->store, md->name, d->p);
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* poll cert */
+
+static void get_up_link(md_proto_driver_t *d, apr_table_t *headers)
+{
+ md_acme_driver_t *ad = d->baton;
+
+ ad->chain_up_link = md_link_find_relation(headers, d->p, "up");
+ if (ad->chain_up_link) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p,
+ "server reports up link as %s", ad->chain_up_link);
+ }
+}
+
+static apr_status_t add_http_certs(apr_array_header_t *chain, apr_pool_t *p,
+ const md_http_response_t *res)
+{
+ apr_status_t rv = APR_SUCCESS;
+ const char *ct;
+
+ ct = apr_table_get(res->headers, "Content-Type");
+ ct = md_util_parse_ct(res->req->pool, ct);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p,
+ "parse certs from %s -> %d (%s)", res->req->url, res->status, ct);
+ if (ct && !strcmp("application/x-pkcs7-mime", ct)) {
+ /* this looks like a root cert and we do not want those in our chain */
+ goto out;
+ }
+
+ /* Lets try to read one or more certificates */
+ if (APR_SUCCESS != (rv = md_cert_chain_read_http(chain, p, res))
+ && APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_EAGAIN;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "cert not in response from %s", res->req->url);
+ }
+out:
+ return rv;
+}
+
+static apr_status_t on_add_cert(md_acme_t *acme, const md_http_response_t *res, void *baton)
+{
+ md_proto_driver_t *d = baton;
+ md_acme_driver_t *ad = d->baton;
+ apr_status_t rv = APR_SUCCESS;
+ int count;
+
+ (void)acme;
+ count = ad->cred->chain->nelts;
+ if (APR_SUCCESS == (rv = add_http_certs(ad->cred->chain, d->p, res))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%d certs parsed",
+ ad->cred->chain->nelts - count);
+ get_up_link(d, res->headers);
+ }
+ return rv;
+}
+
+static apr_status_t get_cert(void *baton, int attempt)
+{
+ md_proto_driver_t *d = baton;
+ md_acme_driver_t *ad = d->baton;
+
+ (void)attempt;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "retrieving cert from %s",
+ ad->order->certificate);
+ return md_acme_GET(ad->acme, ad->order->certificate, NULL, NULL, on_add_cert, NULL, d);
+}
+
+apr_status_t md_acme_drive_cert_poll(md_proto_driver_t *d, int only_once)
+{
+ md_acme_driver_t *ad = d->baton;
+ apr_status_t rv;
+
+ assert(ad->md);
+ assert(ad->acme);
+ assert(ad->order);
+ assert(ad->order->certificate);
+
+ if (only_once) {
+ rv = get_cert(d, 0);
+ }
+ else {
+ rv = md_util_try(get_cert, d, 1, ad->cert_poll_timeout, 0, 0, 1);
+ }
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "poll for cert at %s", ad->order->certificate);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* order finalization */
+
+static apr_status_t on_init_csr_req(md_acme_req_t *req, void *baton)
+{
+ md_proto_driver_t *d = baton;
+ md_acme_driver_t *ad = d->baton;
+ md_json_t *jpayload;
+
+ jpayload = md_json_create(req->p);
+ md_json_sets(ad->csr_der_64, jpayload, MD_KEY_CSR, NULL);
+
+ return md_acme_req_body_init(req, jpayload);
+}
+
+static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void *baton)
+{
+ md_proto_driver_t *d = baton;
+ md_acme_driver_t *ad = d->baton;
+ const char *location;
+ md_cert_t *cert;
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)acme;
+ location = apr_table_get(res->headers, "location");
+ if (!location) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p,
+ "cert created without giving its location header");
+ return APR_EINVAL;
+ }
+ ad->order->certificate = apr_pstrdup(d->p, location);
+ if (APR_SUCCESS != (rv = md_acme_order_save(d->store, d->p, MD_SG_STAGING,
+ d->md->name, ad->order, 0))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p,
+ "%s: saving cert url %s", d->md->name, location);
+ return rv;
+ }
+
+ /* Check if it already was sent with this response */
+ ad->chain_up_link = NULL;
+ if (APR_SUCCESS == (rv = md_cert_read_http(&cert, d->p, res))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed");
+ apr_array_clear(ad->cred->chain);
+ APR_ARRAY_PUSH(ad->cred->chain, md_cert_t*) = cert;
+ get_up_link(d, res->headers);
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ if (location) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p,
+ "cert not in response, need to poll %s", location);
+ }
+ }
+
+ return rv;
+}
+
+/**
+ * Pre-Req: all domains have been validated by the ACME server, e.g. all have AUTHZ
+ * resources that have status 'valid'
+ * - acme_driver->cred keeps the credentials to setup (key spec)
+ * - Setup private key, if not already there
+ * - Generate a CSR with org, contact, etc
+ * - Optionally enable must-staple OCSP extension
+ * - Submit CSR, expect 201 with location
+ * - POLL location for certificate
+ * - store certificate
+ * - retrieve cert chain information from cert
+ * - GET cert chain
+ * - store cert chain
+ */
+apr_status_t md_acme_drive_setup_cred_chain(md_proto_driver_t *d, md_result_t *result)
+{
+ md_acme_driver_t *ad = d->baton;
+ md_pkey_spec_t *spec;
+ md_pkey_t *privkey;
+ apr_status_t rv;
+
+ md_result_activity_printf(result, "Finalizing order for %s", ad->md->name);
+
+ assert(ad->cred);
+ spec = ad->cred->spec;
+
+ rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, spec, &privkey, d->p);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ if (APR_SUCCESS == (rv = md_pkey_gen(&privkey, d->p, spec))) {
+ rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, d->md->name, spec, privkey, 1);
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p,
+ "%s: generate %s privkey", d->md->name, md_pkey_spec_name(spec));
+ }
+ if (APR_SUCCESS != rv) goto leave;
+
+ md_result_activity_printf(result, "Creating %s CSR", md_pkey_spec_name(spec));
+ rv = md_cert_req_create(&ad->csr_der_64, d->md->name, ad->domains,
+ ad->md->must_staple, privkey, d->p);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create %s CSR",
+ d->md->name, md_pkey_spec_name(spec));
+ if (APR_SUCCESS != rv) goto leave;
+
+ md_result_activity_printf(result, "Submitting %s CSR to CA", md_pkey_spec_name(spec));
+ assert(ad->order->finalize);
+ rv = md_acme_POST(ad->acme, ad->order->finalize, on_init_csr_req, NULL, csr_req, NULL, d);
+
+leave:
+ md_acme_report_result(ad->acme, rv, result);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* cert chain retrieval */
+
+static apr_status_t on_add_chain(md_acme_t *acme, const md_http_response_t *res, void *baton)
+{
+ md_proto_driver_t *d = baton;
+ md_acme_driver_t *ad = d->baton;
+ apr_status_t rv = APR_SUCCESS;
+ const char *ct;
+
+ (void)acme;
+ ct = apr_table_get(res->headers, "Content-Type");
+ ct = md_util_parse_ct(res->req->pool, ct);
+ if (ct && !strcmp("application/x-pkcs7-mime", ct)) {
+ /* root cert most likely, end it here */
+ return APR_SUCCESS;
+ }
+
+ if (APR_SUCCESS == (rv = add_http_certs(ad->cred->chain, d->p, res))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain cert parsed");
+ get_up_link(d, res->headers);
+ }
+ return rv;
+}
+
+static apr_status_t get_chain(void *baton, int attempt)
+{
+ md_proto_driver_t *d = baton;
+ md_acme_driver_t *ad = d->baton;
+ const char *prev_link = NULL;
+ apr_status_t rv = APR_SUCCESS;
+
+ while (APR_SUCCESS == rv && ad->cred->chain->nelts < 10) {
+ int nelts = ad->cred->chain->nelts;
+
+ if (ad->chain_up_link && (!prev_link || strcmp(prev_link, ad->chain_up_link))) {
+ prev_link = ad->chain_up_link;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p,
+ "next chain cert at %s", ad->chain_up_link);
+ rv = md_acme_GET(ad->acme, ad->chain_up_link, NULL, NULL, on_add_chain, NULL, d);
+
+ if (APR_SUCCESS == rv && nelts == ad->cred->chain->nelts) {
+ break;
+ }
+ else if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p,
+ "error retrieving certificate from %s", ad->chain_up_link);
+ return rv;
+ }
+ }
+ else if (ad->cred->chain->nelts <= 1) {
+ /* This cannot be the complete chain (no one signs new web certs with their root)
+ * and we did not see a "Link: ...rel=up", so we do not know how to continue. */
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p,
+ "no link header 'up' for new certificate, unable to retrieve chain");
+ rv = APR_EINVAL;
+ break;
+ }
+ else {
+ rv = APR_SUCCESS;
+ break;
+ }
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p,
+ "got chain with %d certs (%d. attempt)", ad->cred->chain->nelts, attempt);
+ return rv;
+}
+
+static apr_status_t ad_chain_retrieve(md_proto_driver_t *d)
+{
+ md_acme_driver_t *ad = d->baton;
+ apr_status_t rv;
+
+ /* This may be called repeatedly and needs to progress. The relevant state is in
+ * ad->cred->chain the certificate chain, starting with the new cert for the md
+ * ad->order->certificate the url where ACME offers us the new md certificate. This may
+ * be a single one or even the complete chain
+ * ad->chain_up_link in case the last certificate retrieval did not end the chain,
+ * the link header with relation "up" gives us the location
+ * for the next cert in the chain
+ */
+ if (md_array_is_empty(ad->cred->chain)) {
+ /* Need to start at the order */
+ ad->chain_up_link = NULL;
+ if (!ad->order) {
+ rv = APR_EGENERAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p,
+ "%s: asked to retrieve chain, but no order in context", d->md->name);
+ goto out;
+ }
+ if (!ad->order->certificate) {
+ rv = APR_EGENERAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p,
+ "%s: asked to retrieve chain, but no certificate url part of order", d->md->name);
+ goto out;
+ }
+
+ if (APR_SUCCESS != (rv = md_acme_drive_cert_poll(d, 0))) {
+ goto out;
+ }
+ }
+
+ rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain retrieved");
+
+out:
+ return rv;
+}
+
+/**************************************************************************************************/
+/* ACME driver init */
+
+static apr_status_t acme_driver_preload_init(md_proto_driver_t *d, md_result_t *result)
+{
+ md_acme_driver_t *ad;
+ md_credentials_t *cred;
+ int i;
+
+ md_result_set(result, APR_SUCCESS, NULL);
+
+ ad = apr_pcalloc(d->p, sizeof(*ad));
+
+ d->baton = ad;
+
+ ad->driver = d;
+ ad->authz_monitor_timeout = apr_time_from_sec(30);
+ ad->cert_poll_timeout = apr_time_from_sec(30);
+ ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char*));
+
+ /* We want to obtain credentials (key+certificate) for every key spec in this MD */
+ ad->creds = apr_array_make(d->p, md_pkeys_spec_count(d->md->pks), sizeof(md_credentials_t*));
+ for (i = 0; i < md_pkeys_spec_count(d->md->pks); ++i) {
+ cred = apr_pcalloc(d->p, sizeof(*cred));
+ cred->spec = md_pkeys_spec_get(d->md->pks, i);
+ cred->chain = apr_array_make(d->p, 5, sizeof(md_cert_t*));
+ APR_ARRAY_PUSH(ad->creds, md_credentials_t*) = cred;
+ }
+
+ 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. */
+ challenge = apr_table_get(d->env, MD_KEY_CHALLENGE);
+ if (challenge) {
+ APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, challenge);
+ }
+ else if (d->md->ca_challenges && d->md->ca_challenges->nelts > 0) {
+ /* pre-configured set for this managed domain */
+ apr_array_cat(ad->ca_challenges, d->md->ca_challenges);
+ }
+ else {
+ /* free to chose. Add all we support and see what we get offered */
+ APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_TLSALPN01;
+ APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_HTTP01;
+ 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)
+ && NULL == d->md->dns01_cmd
+ && 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;
+ }
+ }
+
+ md_result_printf(result, 0, "MDomain %s initialized with support for ACME challenges %s",
+ d->md->name, apr_array_pstrcat(d->p, ad->ca_challenges, ' '));
+
+leave:
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, "%s: init driver", d->md->name);
+ return result->status;
+}
+
+/**************************************************************************************************/
+/* ACME staging */
+
+static apr_status_t load_missing_creds(md_proto_driver_t *d)
+{
+ md_acme_driver_t *ad = d->baton;
+ md_credentials_t *cred;
+ apr_array_header_t *chain;
+ int i, complete;
+ apr_status_t rv;
+
+ complete = 1;
+ for (i = 0; i < ad->creds->nelts; ++i) {
+ rv = APR_SUCCESS;
+ cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*);
+ if (!cred->pkey) {
+ rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, cred->spec, &cred->pkey, d->p);
+ }
+ if (APR_SUCCESS == rv && md_array_is_empty(cred->chain)) {
+ rv = md_pubcert_load(d->store, MD_SG_STAGING, d->md->name, cred->spec, &chain, d->p);
+ if (APR_SUCCESS == rv) {
+ apr_array_cat(cred->chain, chain);
+ }
+ }
+ if (APR_SUCCESS == rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: credentials staged for %s certificate",
+ d->md->name, md_pkey_spec_name(cred->spec));
+ }
+ else {
+ complete = 0;
+ }
+ }
+ return complete? APR_SUCCESS : APR_EAGAIN;
+}
+
+static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result)
+{
+ md_acme_driver_t *ad = d->baton;
+ int reset_staging = d->reset;
+ apr_status_t rv = APR_SUCCESS;
+ apr_time_t now, t, t2;
+ md_credentials_t *cred;
+ const char *ca_effective = NULL;
+ char ts[APR_RFC822_DATE_LEN];
+ int i, first = 0;
+
+ if (!d->md->ca_urls || d->md->ca_urls->nelts <= 0) {
+ /* No CA defined? This is checked in several other places, but lets be sure */
+ md_result_printf(result, APR_INCOMPLETE,
+ "The managed domain %s is missing MDCertificateAuthority", d->md->name);
+ goto out;
+ }
+
+ /* When not explicitly told to reset, we check the existing data. If
+ * it is incomplete or old, we trigger the reset for a clean start. */
+ if (!reset_staging) {
+ md_result_activity_setn(result, "Checking staging area");
+ rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ad->md, d->p);
+ if (APR_SUCCESS == rv) {
+ /* So, we have a copy in staging, but is it a recent or an old one? */
+ if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) {
+ reset_staging = 1;
+ }
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ reset_staging = 1;
+ rv = APR_SUCCESS;
+ }
+ }
+
+ /* What CA are we using this time? */
+ if (ad->md && ad->md->ca_effective) {
+ /* There was one chosen on the previous run. Do we stick to it? */
+ ca_effective = ad->md->ca_effective;
+ if (d->md->ca_urls->nelts > 1 && d->attempt >= d->retry_failover) {
+ /* We have more than one CA to choose from and this is the (at least)
+ * third attempt with the same CA. Let's switch to the next one. */
+ int last_idx = md_array_str_index(d->md->ca_urls, ca_effective, 0, 1);
+ if (last_idx >= 0) {
+ int next_idx = (last_idx+1) % d->md->ca_urls->nelts;
+ ca_effective = APR_ARRAY_IDX(d->md->ca_urls, next_idx, const char*);
+ }
+ else {
+ /* not part of current configuration? */
+ ca_effective = NULL;
+ }
+ /* switching CA means we need to wipe the staging area */
+ reset_staging = 1;
+ }
+ }
+
+ if (!ca_effective) {
+ /* None chosen yet, pick the first one configured */
+ ca_effective = APR_ARRAY_IDX(d->md->ca_urls, 0, const char*);
+ }
+
+ 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, attempt=%d, acme=%s, challenges='%s'",
+ d->md->name, d->md->state, d->attempt, ca_effective,
+ apr_array_pstrcat(d->p, ad->ca_challenges, ' '));
+ }
+
+ if (reset_staging) {
+ md_result_activity_setn(result, "Resetting staging area");
+ /* reset the staging area for this domain */
+ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p,
+ "%s: reset staging area", d->md->name);
+ if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) {
+ md_result_printf(result, rv, "resetting staging area");
+ goto out;
+ }
+ rv = APR_SUCCESS;
+ ad->md = NULL;
+ ad->order = NULL;
+ }
+
+ md_result_activity_setn(result, "Assessing current status");
+ if (ad->md && ad->md->state == MD_S_MISSING_INFORMATION) {
+ /* ToS agreement is missing. It makes no sense to drive this MD further */
+ md_result_printf(result, APR_INCOMPLETE,
+ "The managed domain %s is missing required information", d->md->name);
+ goto out;
+ }
+
+ if (ad->md && APR_SUCCESS == load_missing_creds(d)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: all credentials staged", d->md->name);
+ goto ready;
+ }
+
+ /* Need to renew */
+ if (!ad->md || !md_array_str_eq(ad->md->ca_urls, d->md->ca_urls, 1)) {
+ md_result_activity_printf(result, "Resetting staging for %s", d->md->name);
+ /* re-initialize staging */
+ 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->md->ca_effective = ca_effective;
+ ad->md->ca_account = NULL;
+ ad->order = NULL;
+ rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Saving MD information in staging area.");
+ md_result_log(result, MD_LOG_ERR);
+ goto out;
+ }
+ }
+ if (!ad->domains) {
+ ad->domains = md_dns_make_minimal(d->p, ad->md->domains);
+ }
+
+ md_result_activity_printf(result, "Contacting ACME server for %s at %s",
+ d->md->name, ca_effective);
+ if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, ca_effective,
+ d->proxy_url, d->ca_file))) {
+ md_result_printf(result, rv, "setup ACME communications");
+ md_result_log(result, MD_LOG_ERR);
+ goto out;
+ }
+ if (APR_SUCCESS != (rv = md_acme_setup(ad->acme, result))) {
+ md_result_log(result, MD_LOG_ERR);
+ goto out;
+ }
+
+ if (APR_SUCCESS != load_missing_creds(d)) {
+ for (i = 0; i < ad->creds->nelts; ++i) {
+ ad->cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*);
+ if (!ad->cred->pkey || md_array_is_empty(ad->cred->chain)) {
+ md_result_activity_printf(result, "Driving ACME to renew %s certificate for %s",
+ md_pkey_spec_name(ad->cred->spec),d->md->name);
+ /* The process of setting up challenges and verifying domain
+ * names differs between ACME versions. */
+ switch (MD_ACME_VERSION_MAJOR(ad->acme->version)) {
+ case 1:
+ md_result_printf(result, APR_EINVAL,
+ "ACME server speaks version 1, an obsolete version of the ACME "
+ "protocol that is no longer supported.");
+ rv = result->status;
+ break;
+ default:
+ /* In principle, we only know ACME version 2. But we assume
+ that a new protocol which announces a directory with all members
+ from version 2 will act backward compatible.
+ This is, of course, an assumption...
+ */
+ rv = md_acmev2_drive_renew(ad, d, result);
+ break;
+ }
+ if (APR_SUCCESS != rv) goto out;
+
+ if (md_array_is_empty(ad->cred->chain) || ad->chain_up_link) {
+ md_result_activity_printf(result, "Retrieving %s certificate chain for %s",
+ md_pkey_spec_name(ad->cred->spec), d->md->name);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p,
+ "%s: retrieving %s certificate chain",
+ d->md->name, md_pkey_spec_name(ad->cred->spec));
+ rv = ad_chain_retrieve(d);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Unable to retrieve %s certificate chain.",
+ md_pkey_spec_name(ad->cred->spec));
+ goto out;
+ }
+
+ if (!md_array_is_empty(ad->cred->chain)) {
+
+ if (!ad->cred->pkey) {
+ rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, ad->cred->spec, &ad->cred->pkey, d->p);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Loading the private key.");
+ goto out;
+ }
+ }
+
+ if (ad->cred->pkey) {
+ rv = md_check_cert_and_pkey(ad->cred->chain, ad->cred->pkey);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Certificate and private key do not match.");
+
+ /* Delete the order */
+ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env);
+
+ goto out;
+ }
+ }
+
+ rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, d->md->name,
+ ad->cred->spec, ad->cred->chain, 0);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Saving new %s certificate chain.",
+ md_pkey_spec_name(ad->cred->spec));
+ goto out;
+ }
+ }
+ }
+
+ /* Clean up the order, so the next pkey spec sets up a new one */
+ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env);
+ }
+ }
+ }
+
+
+ /* 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, 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(APR_SUCCESS == load_missing_creds(d));
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p,
+ "%s: certificates ready, activation delay set to %s",
+ d->md->name, md_duration_format(d->p, d->activation_delay));
+
+ /* determine when it should be activated */
+ t = apr_time_now();
+ for (i = 0; i < ad->creds->nelts; ++i) {
+ cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*);
+ t2 = md_cert_get_not_before(APR_ARRAY_IDX(cred->chain, 0, md_cert_t*));
+ if (t2 > t) t = t2;
+ }
+ md_result_delay_set(result, t);
+
+ /* If the existing MD is complete and un-expired, delay the activation
+ * to 24 hours after new cert is valid (if there is enough time left), so
+ * that cients with skewed clocks do not see a problem. */
+ now = apr_time_now();
+ if (d->md->state == MD_S_COMPLETE) {
+ 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 certificates", d->md->name);
+ valid_until = md_reg_valid_until(d->reg, d->md, d->p);
+ 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);
+ }
+ md_result_delay_set(result, result->ready_at + delay_activation);
+ }
+ }
+
+ /* There is a full set staged, to be loaded */
+ apr_rfc822_date(ts, result->ready_at);
+ 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.", ts);
+ }
+ else {
+ md_result_printf(result, APR_SUCCESS,
+ "The certificate for the managed domain has been renewed successfully and can "
+ "be used (valid since %s). A graceful server restart now is recommended.", ts);
+ }
+
+out:
+ return rv;
+}
+
+static apr_status_t acme_driver_renew(md_proto_driver_t *d, md_result_t *result)
+{
+ apr_status_t rv;
+
+ rv = acme_renew(d, result);
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* ACME preload */
+
+static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_group,
+ const char *name, md_result_t *result)
+{
+ apr_status_t rv;
+ md_pkey_t *acct_key;
+ md_t *md;
+ md_pkey_spec_t *pkspec;
+ md_credentials_t *creds;
+ apr_array_header_t *all_creds;
+ struct md_acme_acct_t *acct;
+ const char *id;
+ int i;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name);
+ /* Load data from MD_SG_STAGING and save it into "load_group".
+ * This serves several purposes:
+ * 1. It's a format check on the input data.
+ * 2. We write back what we read, creating data with our own access permissions
+ * 3. We ignore any other accumulated data in STAGING
+ * 4. Once "load_group" is complete an ok, we can swap/archive groups with a rename
+ * 5. Reading/Writing the data will apply/remove any group specific data encryption.
+ */
+ if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) {
+ md_result_set(result, rv, "loading staged md.json");
+ goto leave;
+ }
+ if (!md->ca_effective) {
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "effective CA url not set");
+ goto leave;
+ }
+
+ all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*));
+ for (i = 0; i < md_pkeys_spec_count(md->pks); ++i) {
+ pkspec = md_pkeys_spec_get(md->pks, i);
+ if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) {
+ md_result_printf(result, rv, "loading staged credentials #%d", i);
+ goto leave;
+ }
+ if (!creds->chain) {
+ rv = APR_ENOENT;
+ md_result_printf(result, rv, "no certificate in staged credentials #%d", i);
+ goto leave;
+ }
+ if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) {
+ md_result_printf(result, rv, "certificate and private key do not match in staged credentials #%d", i);
+ goto leave;
+ }
+ APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds;
+ }
+
+ /* See if staging holds a new or modified account data */
+ rv = md_acme_acct_load(&acct, &acct_key, d->store, MD_SG_STAGING, name, d->p);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ acct = NULL;
+ acct_key = NULL;
+ rv = APR_SUCCESS;
+ }
+ else if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "loading staged account");
+ goto leave;
+ }
+
+ md_result_activity_setn(result, "purging order information");
+ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, md, d->env);
+
+ md_result_activity_setn(result, "purging store tmp space");
+ rv = md_store_purge(d->store, d->p, load_group, name);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, NULL);
+ goto leave;
+ }
+
+ if (acct) {
+ md_acme_t *acme;
+
+ /* We may have STAGED the same account several times. This happens when
+ * several MDs are renewed at once and need a new account. They will all store
+ * the new account in their own STAGING area. By checking for accounts with
+ * the same url, we save them all into a single one.
+ */
+ md_result_activity_setn(result, "saving staged account");
+ id = md->ca_account;
+ if (!id) {
+ rv = md_acme_acct_id_for_md(&id, d->store, MD_SG_ACCOUNTS, md, d->p);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ id = NULL;
+ }
+ else if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "error searching for existing account by url");
+ goto leave;
+ }
+ }
+
+ if (APR_SUCCESS != (rv = md_acme_create(&acme, d->p, md->ca_effective,
+ d->proxy_url, d->ca_file))) {
+ md_result_set(result, rv, "error setting up acme");
+ goto leave;
+ }
+
+ if (APR_SUCCESS != (rv = md_acme_acct_save(d->store, d->p, acme, &id, acct, acct_key))) {
+ md_result_set(result, rv, "error saving account");
+ goto leave;
+ }
+ md->ca_account = id;
+ }
+ else if (!md->ca_account) {
+ /* staging reused another account and did not create a new one. find
+ * the account, if it is already there */
+ rv = md_acme_acct_id_for_md(&id, d->store, MD_SG_ACCOUNTS, md, d->p);
+ if (APR_SUCCESS == rv) {
+ md->ca_account = id;
+ }
+ }
+
+ md_result_activity_setn(result, "saving staged md/privkey/pubcert");
+ if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) {
+ md_result_set(result, rv, "writing md.json");
+ goto leave;
+ }
+
+ for (i = 0; i < all_creds->nelts; ++i) {
+ creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*);
+ if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) {
+ md_result_printf(result, rv, "writing credentials #%d", i);
+ goto leave;
+ }
+ }
+
+ md_result_set(result, APR_SUCCESS, "saved staged data successfully");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t acme_driver_preload(md_proto_driver_t *d,
+ md_store_group_t group, md_result_t *result)
+{
+ apr_status_t rv;
+
+ rv = acme_preload(d, group, d->md->name, result);
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p)
+{
+ (void)p;
+ if (!md->ca_urls || apr_is_empty_array(md->ca_urls)) {
+ md->ca_urls = apr_array_make(p, 3, sizeof(const char *));
+ APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_ACME_DEF_URL;
+ }
+ return APR_SUCCESS;
+}
+
+static md_proto_t ACME_PROTO = {
+ MD_PROTO_ACME, acme_driver_init, acme_driver_renew,
+ acme_driver_preload_init, acme_driver_preload,
+ acme_complete_md,
+};
+
+apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p)
+{
+ (void)p;
+ apr_hash_set(protos, MD_PROTO_ACME, sizeof(MD_PROTO_ACME)-1, &ACME_PROTO);
+ return APR_SUCCESS;
+}
diff --git a/modules/md/md_acme_drive.h b/modules/md/md_acme_drive.h
new file mode 100644
index 0000000..88761fa
--- /dev/null
+++ b/modules/md/md_acme_drive.h
@@ -0,0 +1,55 @@
+/* Copyright 2019 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed 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_acme_drive_h
+#define md_acme_drive_h
+
+struct apr_array_header_t;
+struct md_acme_order_t;
+struct md_credentials_t;
+struct md_result_t;
+
+typedef struct md_acme_driver_t {
+ md_proto_driver_t *driver;
+ void *sub_driver;
+
+ md_acme_t *acme;
+ md_t *md;
+ struct apr_array_header_t *domains;
+ apr_array_header_t *ca_challenges;
+
+ int complete;
+ apr_array_header_t *creds; /* the new md_credentials_t */
+
+ struct md_credentials_t *cred; /* credentials currently being processed */
+ const char *chain_up_link; /* Link header "up" from last chain retrieval,
+ needs to be followed */
+
+ struct md_acme_order_t *order;
+ apr_interval_time_t authz_monitor_timeout;
+
+ const char *csr_der_64;
+ apr_interval_time_t cert_poll_timeout;
+
+} md_acme_driver_t;
+
+apr_status_t md_acme_drive_set_acct(struct md_proto_driver_t *d,
+ struct md_result_t *result);
+apr_status_t md_acme_drive_setup_cred_chain(struct md_proto_driver_t *d,
+ struct md_result_t *result);
+apr_status_t md_acme_drive_cert_poll(struct md_proto_driver_t *d, int only_once);
+
+#endif /* md_acme_drive_h */
+
diff --git a/modules/md/md_acme_order.c b/modules/md/md_acme_order.c
new file mode 100644
index 0000000..9e25e84
--- /dev/null
+++ b/modules/md/md_acme_order.c
@@ -0,0 +1,562 @@
+/* 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 <apr_lib.h>
+#include <apr_buckets.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_jws.h"
+#include "md_result.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_authz.h"
+#include "md_acme_order.h"
+
+
+md_acme_order_t *md_acme_order_create(apr_pool_t *p)
+{
+ md_acme_order_t *order;
+
+ order = apr_pcalloc(p, sizeof(*order));
+ order->p = p;
+ order->authz_urls = apr_array_make(p, 5, sizeof(const char *));
+ order->challenge_setups = apr_array_make(p, 5, sizeof(const char *));
+
+ return order;
+}
+
+/**************************************************************************************************/
+/* order conversion */
+
+#define MD_KEY_CHALLENGE_SETUPS "challenge-setups"
+
+static md_acme_order_st order_st_from_str(const char *s)
+{
+ if (s) {
+ if (!strcmp("valid", s)) {
+ return MD_ACME_ORDER_ST_VALID;
+ }
+ else if (!strcmp("invalid", s)) {
+ return MD_ACME_ORDER_ST_INVALID;
+ }
+ else if (!strcmp("ready", s)) {
+ return MD_ACME_ORDER_ST_READY;
+ }
+ else if (!strcmp("pending", s)) {
+ return MD_ACME_ORDER_ST_PENDING;
+ }
+ else if (!strcmp("processing", s)) {
+ return MD_ACME_ORDER_ST_PROCESSING;
+ }
+ }
+ return MD_ACME_ORDER_ST_PENDING;
+}
+
+static const char *order_st_to_str(md_acme_order_st status)
+{
+ switch (status) {
+ case MD_ACME_ORDER_ST_PENDING:
+ return "pending";
+ case MD_ACME_ORDER_ST_READY:
+ return "ready";
+ case MD_ACME_ORDER_ST_PROCESSING:
+ return "processing";
+ case MD_ACME_ORDER_ST_VALID:
+ return "valid";
+ case MD_ACME_ORDER_ST_INVALID:
+ return "invalid";
+ default:
+ return "invalid";
+ }
+}
+
+md_json_t *md_acme_order_to_json(md_acme_order_t *order, apr_pool_t *p)
+{
+ md_json_t *json = md_json_create(p);
+
+ if (order->url) {
+ md_json_sets(order->url, json, MD_KEY_URL, NULL);
+ }
+ md_json_sets(order_st_to_str(order->status), json, MD_KEY_STATUS, NULL);
+ md_json_setsa(order->authz_urls, json, MD_KEY_AUTHORIZATIONS, NULL);
+ md_json_setsa(order->challenge_setups, json, MD_KEY_CHALLENGE_SETUPS, NULL);
+ if (order->finalize) {
+ md_json_sets(order->finalize, json, MD_KEY_FINALIZE, NULL);
+ }
+ if (order->certificate) {
+ md_json_sets(order->certificate, json, MD_KEY_CERTIFICATE, NULL);
+ }
+ return json;
+}
+
+static void order_update_from_json(md_acme_order_t *order, md_json_t *json, apr_pool_t *p)
+{
+ if (!order->url && md_json_has_key(json, MD_KEY_URL, NULL)) {
+ order->url = md_json_dups(p, json, MD_KEY_URL, NULL);
+ }
+ order->status = order_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL));
+ if (md_json_has_key(json, MD_KEY_AUTHORIZATIONS, NULL)) {
+ md_json_dupsa(order->authz_urls, p, json, MD_KEY_AUTHORIZATIONS, NULL);
+ }
+ if (md_json_has_key(json, MD_KEY_CHALLENGE_SETUPS, NULL)) {
+ md_json_dupsa(order->challenge_setups, p, json, MD_KEY_CHALLENGE_SETUPS, NULL);
+ }
+ if (md_json_has_key(json, MD_KEY_FINALIZE, NULL)) {
+ order->finalize = md_json_dups(p, json, MD_KEY_FINALIZE, NULL);
+ }
+ if (md_json_has_key(json, MD_KEY_CERTIFICATE, NULL)) {
+ order->certificate = md_json_dups(p, json, MD_KEY_CERTIFICATE, NULL);
+ }
+}
+
+md_acme_order_t *md_acme_order_from_json(md_json_t *json, apr_pool_t *p)
+{
+ md_acme_order_t *order = md_acme_order_create(p);
+
+ order_update_from_json(order, json, p);
+ return order;
+}
+
+apr_status_t md_acme_order_add(md_acme_order_t *order, const char *authz_url)
+{
+ assert(authz_url);
+ if (md_array_str_index(order->authz_urls, authz_url, 0, 1) < 0) {
+ APR_ARRAY_PUSH(order->authz_urls, const char*) = apr_pstrdup(order->p, authz_url);
+ }
+ return APR_SUCCESS;
+}
+
+apr_status_t md_acme_order_remove(md_acme_order_t *order, const char *authz_url)
+{
+ int i;
+
+ assert(authz_url);
+ i = md_array_str_index(order->authz_urls, authz_url, 0, 1);
+ if (i >= 0) {
+ order->authz_urls = md_array_str_remove(order->p, order->authz_urls, authz_url, 1);
+ return APR_SUCCESS;
+ }
+ return APR_ENOENT;
+}
+
+static apr_status_t add_setup_token(md_acme_order_t *order, const char *token)
+{
+ if (md_array_str_index(order->challenge_setups, token, 0, 1) < 0) {
+ APR_ARRAY_PUSH(order->challenge_setups, const char*) = apr_pstrdup(order->p, token);
+ }
+ return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* persistence */
+
+apr_status_t md_acme_order_load(struct md_store_t *store, md_store_group_t group,
+ const char *md_name, md_acme_order_t **pauthz_set,
+ apr_pool_t *p)
+{
+ apr_status_t rv;
+ md_json_t *json;
+ md_acme_order_t *authz_set;
+
+ rv = md_store_load_json(store, group, md_name, MD_FN_ORDER, &json, p);
+ if (APR_SUCCESS == rv) {
+ authz_set = md_acme_order_from_json(json, p);
+ }
+ *pauthz_set = (APR_SUCCESS == rv)? authz_set : NULL;
+ return rv;
+}
+
+static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_t *store = baton;
+ md_json_t *json;
+ md_store_group_t group;
+ md_acme_order_t *set;
+ const char *md_name;
+ int create;
+
+ (void)p;
+ group = (md_store_group_t)va_arg(ap, int);
+ md_name = va_arg(ap, const char *);
+ set = va_arg(ap, md_acme_order_t *);
+ create = va_arg(ap, int);
+
+ json = md_acme_order_to_json(set, ptemp);
+ assert(json);
+ return md_store_save_json(store, ptemp, group, md_name, MD_FN_ORDER, json, create);
+}
+
+apr_status_t md_acme_order_save(struct md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const char *md_name,
+ md_acme_order_t *authz_set, int create)
+{
+ return md_util_pool_vdo(p_save, store, p, group, md_name, authz_set, create, NULL);
+}
+
+static apr_status_t p_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_t *store = baton;
+ md_acme_order_t *order;
+ md_store_group_t group;
+ const md_t *md;
+ const char *setup_token;
+ apr_table_t *env;
+ int i;
+
+ group = (md_store_group_t)va_arg(ap, int);
+ md = va_arg(ap, const md_t *);
+ env = va_arg(ap, apr_table_t *);
+
+ if (APR_SUCCESS == md_acme_order_load(store, group, md->name, &order, p)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "order loaded for %s", md->name);
+ for (i = 0; i < order->challenge_setups->nelts; ++i) {
+ setup_token = APR_ARRAY_IDX(order->challenge_setups, i, const char*);
+ if (setup_token) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p,
+ "order teardown setup %s", setup_token);
+ md_acme_authz_teardown(store, setup_token, md, env, p);
+ }
+ }
+ }
+ return md_store_remove(store, group, md->name, MD_FN_ORDER, ptemp, 1);
+}
+
+apr_status_t md_acme_order_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ const md_t *md, apr_table_t *env)
+{
+ return md_util_pool_vdo(p_purge, store, p, group, md, env, NULL);
+}
+
+/**************************************************************************************************/
+/* ACMEv2 order requests */
+
+typedef struct {
+ apr_pool_t *p;
+ md_acme_order_t *order;
+ md_acme_t *acme;
+ const char *name;
+ apr_array_header_t *domains;
+ md_result_t *result;
+} order_ctx_t;
+
+#define ORDER_CTX_INIT(ctx, p, o, a, n, d, r) \
+ (ctx)->p = (p); (ctx)->order = (o); (ctx)->acme = (a); \
+ (ctx)->name = (n); (ctx)->domains = d; (ctx)->result = r
+
+static apr_status_t identifier_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+ md_json_t *jid;
+
+ (void)baton;
+ jid = md_json_create(p);
+ md_json_sets("dns", jid, "type", NULL);
+ md_json_sets(value, jid, "value", NULL);
+ return md_json_setj(jid, json, NULL);
+}
+
+static apr_status_t on_init_order_register(md_acme_req_t *req, void *baton)
+{
+ order_ctx_t *ctx = baton;
+ md_json_t *jpayload;
+
+ jpayload = md_json_create(req->p);
+ md_json_seta(ctx->domains, identifier_to_json, NULL, jpayload, "identifiers", NULL);
+
+ return md_acme_req_body_init(req, jpayload);
+}
+
+static apr_status_t on_order_upd(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs,
+ md_json_t *body, void *baton)
+{
+ order_ctx_t *ctx = baton;
+ const char *location = apr_table_get(hdrs, "location");
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)acme;
+ (void)p;
+ if (!ctx->order) {
+ if (location) {
+ ctx->order = md_acme_order_create(ctx->p);
+ ctx->order->url = apr_pstrdup(ctx->p, location);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ctx->p, "new order at %s", location);
+ }
+ else {
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ctx->p, "new order, no location header");
+ goto out;
+ }
+ }
+
+ order_update_from_json(ctx->order, body, ctx->p);
+out:
+ return rv;
+}
+
+apr_status_t md_acme_order_register(md_acme_order_t **porder, md_acme_t *acme, apr_pool_t *p,
+ const char *name, apr_array_header_t *domains)
+{
+ order_ctx_t ctx;
+ apr_status_t rv;
+
+ assert(MD_ACME_VERSION_MAJOR(acme->version) > 1);
+ ORDER_CTX_INIT(&ctx, p, NULL, acme, name, domains, NULL);
+ rv = md_acme_POST(acme, acme->api.v2.new_order, on_init_order_register, on_order_upd, NULL, NULL, &ctx);
+ *porder = (APR_SUCCESS == rv)? ctx.order : NULL;
+ return rv;
+}
+
+apr_status_t md_acme_order_update(md_acme_order_t *order, md_acme_t *acme,
+ md_result_t *result, apr_pool_t *p)
+{
+ order_ctx_t ctx;
+ apr_status_t rv;
+
+ assert(MD_ACME_VERSION_MAJOR(acme->version) > 1);
+ ORDER_CTX_INIT(&ctx, p, order, acme, NULL, NULL, result);
+ rv = md_acme_GET(acme, order->url, NULL, on_order_upd, NULL, NULL, &ctx);
+ if (APR_SUCCESS != rv && APR_SUCCESS != acme->last->status) {
+ md_result_dup(result, acme->last);
+ }
+ return rv;
+}
+
+static apr_status_t await_ready(void *baton, int attempt)
+{
+ order_ctx_t *ctx = baton;
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)attempt;
+ if (APR_SUCCESS != (rv = md_acme_order_update(ctx->order, ctx->acme,
+ ctx->result, ctx->p))) goto out;
+ switch (ctx->order->status) {
+ case MD_ACME_ORDER_ST_READY:
+ case MD_ACME_ORDER_ST_PROCESSING:
+ case MD_ACME_ORDER_ST_VALID:
+ break;
+ case MD_ACME_ORDER_ST_PENDING:
+ rv = APR_EAGAIN;
+ break;
+ default:
+ rv = APR_EINVAL;
+ break;
+ }
+out:
+ return rv;
+}
+
+apr_status_t md_acme_order_await_ready(md_acme_order_t *order, md_acme_t *acme,
+ const md_t *md, apr_interval_time_t timeout,
+ md_result_t *result, apr_pool_t *p)
+{
+ order_ctx_t ctx;
+ apr_status_t rv;
+
+ assert(MD_ACME_VERSION_MAJOR(acme->version) > 1);
+ ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, result);
+
+ md_result_activity_setn(result, "Waiting for order to become ready");
+ rv = md_util_try(await_ready, &ctx, 0, timeout, 0, 0, 1);
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t await_valid(void *baton, int attempt)
+{
+ order_ctx_t *ctx = baton;
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)attempt;
+ if (APR_SUCCESS != (rv = md_acme_order_update(ctx->order, ctx->acme,
+ ctx->result, ctx->p))) goto out;
+ switch (ctx->order->status) {
+ case MD_ACME_ORDER_ST_VALID:
+ md_result_set(ctx->result, APR_EINVAL, "ACME server order status is 'valid'.");
+ break;
+ case MD_ACME_ORDER_ST_PROCESSING:
+ rv = APR_EAGAIN;
+ break;
+ case MD_ACME_ORDER_ST_INVALID:
+ md_result_set(ctx->result, APR_EINVAL, "ACME server order status is 'invalid'.");
+ rv = APR_EINVAL;
+ break;
+ default:
+ rv = APR_EINVAL;
+ break;
+ }
+out:
+ return rv;
+}
+
+apr_status_t md_acme_order_await_valid(md_acme_order_t *order, md_acme_t *acme,
+ const md_t *md, apr_interval_time_t timeout,
+ md_result_t *result, apr_pool_t *p)
+{
+ order_ctx_t ctx;
+ apr_status_t rv;
+
+ assert(MD_ACME_VERSION_MAJOR(acme->version) > 1);
+ ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, result);
+
+ md_result_activity_setn(result, "Waiting for finalized order to become valid");
+ rv = md_util_try(await_valid, &ctx, 0, timeout, 0, 0, 1);
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* processing */
+
+apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *acme,
+ apr_array_header_t *challenge_types,
+ md_store_t *store, const md_t *md,
+ apr_table_t *env, md_result_t *result,
+ apr_pool_t *p)
+{
+ apr_status_t rv = APR_SUCCESS;
+ md_acme_authz_t *authz;
+ const char *url, *setup_token;
+ int i;
+
+ md_result_activity_printf(result, "Starting challenges for domains");
+ for (i = 0; i < order->authz_urls->nelts; ++i) {
+ url = APR_ARRAY_IDX(order->authz_urls, i, const char*);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: check AUTHZ at %s", md->name, url);
+
+ if (APR_SUCCESS != (rv = md_acme_authz_retrieve(acme, p, url, &authz))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: check authz for %s",
+ md->name, authz->domain);
+ goto leave;
+ }
+
+ switch (authz->state) {
+ case MD_ACME_AUTHZ_S_VALID:
+ break;
+
+ case MD_ACME_AUTHZ_S_PENDING:
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "%s: authorization pending for %s",
+ md->name, authz->domain);
+ rv = md_acme_authz_respond(authz, acme, store, challenge_types,
+ md->pks,
+ md->acme_tls_1_domains, md,
+ env, p, &setup_token, result);
+ if (APR_SUCCESS != rv) {
+ 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 leave;
+ }
+ }
+leave:
+ return rv;
+}
+
+static apr_status_t check_challenges(void *baton, int attempt)
+{
+ order_ctx_t *ctx = baton;
+ const char *url;
+ md_acme_authz_t *authz;
+ apr_status_t rv = APR_SUCCESS;
+ int i;
+
+ for (i = 0; i < ctx->order->authz_urls->nelts; ++i) {
+ url = APR_ARRAY_IDX(ctx->order->authz_urls, i, const char*);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ctx->p, "%s: check AUTHZ at %s (attempt %d)",
+ ctx->name, url, attempt);
+
+ rv = md_acme_authz_retrieve(ctx->acme, ctx->p, url, &authz);
+ if (APR_SUCCESS == rv) {
+ switch (authz->state) {
+ case MD_ACME_AUTHZ_S_VALID:
+ md_result_printf(ctx->result, rv,
+ "domain authorization for %s is valid", authz->domain);
+ break;
+ case MD_ACME_AUTHZ_S_PENDING:
+ rv = APR_EAGAIN;
+ 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;
+ md_result_printf(ctx->result, rv,
+ "domain authorization for %s failed, CA considers "
+ "answer to challenge invalid%s.",
+ authz->domain, authz->error_type? "" : ", no error given");
+ md_result_log(ctx->result, MD_LOG_ERR);
+ goto leave;
+ default:
+ rv = APR_EINVAL;
+ md_result_printf(ctx->result, rv,
+ "domain authorization for %s failed with state %d",
+ authz->domain, authz->state);
+ md_result_log(ctx->result, MD_LOG_ERR);
+ goto leave;
+ }
+ }
+ else {
+ md_result_printf(ctx->result, rv, "authorization retrieval failed for domain %s",
+ authz->domain);
+ }
+ }
+leave:
+ return rv;
+}
+
+apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acme,
+ const md_t *md, apr_interval_time_t timeout,
+ md_result_t *result, apr_pool_t *p)
+{
+ order_ctx_t ctx;
+ apr_status_t rv;
+
+ ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, result);
+
+ 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_DEBUG, rv, p, "%s: checked authorizations", md->name);
+ return rv;
+}
+
diff --git a/modules/md/md_acme_order.h b/modules/md/md_acme_order.h
new file mode 100644
index 0000000..4170440
--- /dev/null
+++ b/modules/md/md_acme_order.h
@@ -0,0 +1,91 @@
+/* Copyright 2019 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed 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_acme_order_h
+#define md_acme_order_h
+
+struct md_json_t;
+struct md_result_t;
+
+typedef struct md_acme_order_t md_acme_order_t;
+
+typedef enum {
+ MD_ACME_ORDER_ST_PENDING,
+ MD_ACME_ORDER_ST_READY,
+ MD_ACME_ORDER_ST_PROCESSING,
+ MD_ACME_ORDER_ST_VALID,
+ MD_ACME_ORDER_ST_INVALID,
+} md_acme_order_st;
+
+struct md_acme_order_t {
+ apr_pool_t *p;
+ const char *url;
+ md_acme_order_st status;
+ struct apr_array_header_t *authz_urls;
+ struct apr_array_header_t *challenge_setups;
+ struct md_json_t *json;
+ const char *finalize;
+ const char *certificate;
+};
+
+#define MD_FN_ORDER "order.json"
+
+/**************************************************************************************************/
+
+md_acme_order_t *md_acme_order_create(apr_pool_t *p);
+
+apr_status_t md_acme_order_add(md_acme_order_t *order, const char *authz_url);
+apr_status_t md_acme_order_remove(md_acme_order_t *order, const char *authz_url);
+
+struct md_json_t *md_acme_order_to_json(md_acme_order_t *set, apr_pool_t *p);
+md_acme_order_t *md_acme_order_from_json(struct md_json_t *json, apr_pool_t *p);
+
+apr_status_t md_acme_order_load(struct md_store_t *store, md_store_group_t group,
+ const char *md_name, md_acme_order_t **pauthz_set,
+ apr_pool_t *p);
+apr_status_t md_acme_order_save(struct md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const char *md_name,
+ md_acme_order_t *authz_set, int create);
+
+apr_status_t md_acme_order_purge(struct md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const md_t *md,
+ apr_table_t *env);
+
+apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *acme,
+ apr_array_header_t *challenge_types,
+ md_store_t *store, const md_t *md,
+ apr_table_t *env, struct md_result_t *result,
+ apr_pool_t *p);
+
+apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acme,
+ const md_t *md, apr_interval_time_t timeout,
+ struct md_result_t *result, apr_pool_t *p);
+
+/* ACMEv2 only ************************************************************************************/
+
+apr_status_t md_acme_order_register(md_acme_order_t **porder, md_acme_t *acme, apr_pool_t *p,
+ const char *name, struct apr_array_header_t *domains);
+
+apr_status_t md_acme_order_update(md_acme_order_t *order, md_acme_t *acme,
+ struct md_result_t *result, apr_pool_t *p);
+
+apr_status_t md_acme_order_await_ready(md_acme_order_t *order, md_acme_t *acme,
+ const md_t *md, apr_interval_time_t timeout,
+ struct md_result_t *result, apr_pool_t *p);
+apr_status_t md_acme_order_await_valid(md_acme_order_t *order, md_acme_t *acme,
+ const md_t *md, apr_interval_time_t timeout,
+ struct md_result_t *result, apr_pool_t *p);
+
+#endif /* md_acme_order_h */
diff --git a/modules/md/md_acmev2_drive.c b/modules/md/md_acmev2_drive.c
new file mode 100644
index 0000000..9dfca96
--- /dev/null
+++ b/modules/md/md_acmev2_drive.c
@@ -0,0 +1,181 @@
+/* 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 <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+#include "md_acme_authz.h"
+#include "md_acme_order.h"
+
+#include "md_acme_drive.h"
+#include "md_acmev2_drive.h"
+
+
+
+/**************************************************************************************************/
+/* order setup */
+
+/**
+ * Either we have an order stored in the STAGING area, or we need to create a
+ * new one at the ACME server.
+ */
+static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, int *pis_new)
+{
+ md_acme_driver_t *ad = d->baton;
+ apr_status_t rv;
+ md_t *md = ad->md;
+
+ assert(ad->md);
+ assert(ad->acme);
+
+ /* For each domain in MD: AUTHZ setup
+ * if an AUTHZ resource is known, check if it is still valid
+ * if known AUTHZ resource is not valid, remove, goto 4.1.1
+ * if no AUTHZ available, create a new one for the domain, store it
+ */
+ if (pis_new) *pis_new = 0;
+ rv = md_acme_order_load(d->store, MD_SG_STAGING, md->name, &ad->order, d->p);
+ if (APR_SUCCESS == rv) {
+ md_result_activity_setn(result, "Loaded order from staging");
+ goto leave;
+ }
+ else if (!APR_STATUS_IS_ENOENT(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: loading order", md->name);
+ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, md, d->env);
+ }
+
+ md_result_activity_setn(result, "Creating new order");
+ rv = md_acme_order_register(&ad->order, ad->acme, d->p, d->md->name, ad->domains);
+ if (APR_SUCCESS !=rv) goto leave;
+ rv = md_acme_order_save(d->store, d->p, MD_SG_STAGING, d->md->name, ad->order, 0);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "saving order in staging");
+ }
+ if (pis_new) *pis_new = 1;
+
+leave:
+ md_acme_report_result(ad->acme, rv, result);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* ACMEv2 renewal */
+
+apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, md_result_t *result)
+{
+ apr_status_t rv = APR_SUCCESS;
+ int is_new_order = 0;
+
+ 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);
+ if (APR_SUCCESS != rv) goto leave;
+
+ if (!md_array_is_empty(ad->cred->chain)) goto leave;
+
+ /* ACMEv2 strategy:
+ * 1. load an md_acme_order_t from STAGING, if present
+ * 2. if no order found, register a new order at ACME server
+ * 3. update the order from the server
+ * 4. Switch order state:
+ * * PENDING: process authz challenges
+ * * READY: finalize the order
+ * * PROCESSING: wait and re-assses later
+ * * VALID: retrieve certificate
+ * * COMPLETE: all done, return success
+ * * INVALID and otherwise: fail renewal, delete local order
+ */
+ if (APR_SUCCESS != (rv = ad_setup_order(d, result, &is_new_order))) {
+ goto leave;
+ }
+
+ rv = md_acme_order_update(ad->order, ad->acme, result, d->p);
+ if (APR_STATUS_IS_ENOENT(rv)
+ || APR_STATUS_IS_EACCES(rv)
+ || MD_ACME_ORDER_ST_INVALID == ad->order->status) {
+ /* order is invalid or no longer known at the ACME server */
+ ad->order = NULL;
+ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env);
+ }
+ else if (APR_SUCCESS != rv) {
+ goto leave;
+ }
+
+retry:
+ if (!ad->order) {
+ rv = ad_setup_order(d, result, &is_new_order);
+ if (APR_SUCCESS != rv) goto leave;
+ }
+
+ rv = md_acme_order_start_challenges(ad->order, ad->acme, ad->ca_challenges,
+ d->store, d->md, d->env, result, d->p);
+ if (!is_new_order && APR_STATUS_IS_EINVAL(rv)) {
+ /* found 'invalid' domains in previous order, need to start over */
+ ad->order = NULL;
+ md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env);
+ goto retry;
+ }
+ if (APR_SUCCESS != rv) goto leave;
+
+ rv = md_acme_order_monitor_authzs(ad->order, ad->acme, d->md,
+ ad->authz_monitor_timeout, result, d->p);
+ if (APR_SUCCESS != rv) goto leave;
+
+ rv = md_acme_order_await_ready(ad->order, ad->acme, d->md,
+ ad->authz_monitor_timeout, result, d->p);
+ if (APR_SUCCESS != rv) goto leave;
+
+ if (MD_ACME_ORDER_ST_READY == ad->order->status) {
+ rv = md_acme_drive_setup_cred_chain(d, result);
+ if (APR_SUCCESS != rv) goto leave;
+ 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);
+ if (APR_SUCCESS != rv) goto leave;
+
+ if (!ad->order->certificate) {
+ md_result_set(result, APR_EINVAL, "Order valid, but certificate url is missing.");
+ goto leave;
+ }
+ md_result_set(result, APR_SUCCESS, NULL);
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return result->status;
+}
+
diff --git a/modules/md/md_acmev2_drive.h b/modules/md/md_acmev2_drive.h
new file mode 100644
index 0000000..7552c4f
--- /dev/null
+++ b/modules/md/md_acmev2_drive.h
@@ -0,0 +1,27 @@
+/* Copyright 2019 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed 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_acmev2_drive_h
+#define md_acmev2_drive_h
+
+struct md_json_t;
+struct md_proto_driver_t;
+struct md_result_t;
+
+apr_status_t md_acmev2_drive_renew(struct md_acme_driver_t *ad,
+ struct md_proto_driver_t *d,
+ struct md_result_t *result);
+
+#endif /* md_acmev2_drive_h */
diff --git a/modules/md/md_core.c b/modules/md/md_core.c
new file mode 100644
index 0000000..7aacff0
--- /dev/null
+++ b/modules/md/md_core.c
@@ -0,0 +1,462 @@
+/* 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 <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_uri.h>
+#include <apr_tables.h>
+#include <apr_time.h>
+#include <apr_date.h>
+
+#include "md_json.h"
+#include "md.h"
+#include "md_crypt.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_util.h"
+
+
+int md_contains(const md_t *md, const char *domain, int case_sensitive)
+{
+ if (md_array_str_index(md->domains, domain, 0, case_sensitive) >= 0) {
+ return 1;
+ }
+ return md_dns_domains_match(md->domains, domain);
+}
+
+const char *md_common_name(const md_t *md1, const md_t *md2)
+{
+ int i;
+
+ if (md1 == NULL || md1->domains == NULL
+ || md2 == NULL || md2->domains == NULL) {
+ return NULL;
+ }
+
+ for (i = 0; i < md1->domains->nelts; ++i) {
+ const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*);
+ if (md_contains(md2, name1, 0)) {
+ return name1;
+ }
+ }
+ return NULL;
+}
+
+int md_domains_overlap(const md_t *md1, const md_t *md2)
+{
+ return md_common_name(md1, md2) != NULL;
+}
+
+apr_size_t md_common_name_count(const md_t *md1, const md_t *md2)
+{
+ int i;
+ apr_size_t hits;
+
+ if (md1 == NULL || md1->domains == NULL
+ || md2 == NULL || md2->domains == NULL) {
+ return 0;
+ }
+
+ hits = 0;
+ for (i = 0; i < md1->domains->nelts; ++i) {
+ const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*);
+ if (md_contains(md2, name1, 0)) {
+ ++hits;
+ }
+ }
+ return hits;
+}
+
+int md_is_covered_by_alt_names(const md_t *md, const struct apr_array_header_t* alt_names)
+{
+ const char *name;
+ int i;
+
+ if (alt_names) {
+ for (i = 0; i < md->domains->nelts; ++i) {
+ name = APR_ARRAY_IDX(md->domains, i, const char *);
+ if (!md_dns_domains_match(alt_names, name)) {
+ return 0;
+ }
+ }
+ return 1;
+ }
+ return 0;
+}
+
+md_t *md_create_empty(apr_pool_t *p)
+{
+ md_t *md = apr_pcalloc(p, sizeof(*md));
+ if (md) {
+ md->domains = apr_array_make(p, 5, sizeof(const char *));
+ md->contacts = apr_array_make(p, 5, sizeof(const char *));
+ md->renew_mode = MD_RENEW_DEFAULT;
+ md->require_https = MD_REQUIRE_UNSET;
+ 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;
+ }
+ return md;
+}
+
+int md_equal_domains(const md_t *md1, const md_t *md2, int case_sensitive)
+{
+ int i;
+ if (md1->domains->nelts == md2->domains->nelts) {
+ for (i = 0; i < md1->domains->nelts; ++i) {
+ const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*);
+ if (!md_contains(md2, name1, case_sensitive)) {
+ return 0;
+ }
+ }
+ return 1;
+ }
+ return 0;
+}
+
+int md_contains_domains(const md_t *md1, const md_t *md2)
+{
+ int i;
+ if (md1->domains->nelts >= md2->domains->nelts) {
+ for (i = 0; i < md2->domains->nelts; ++i) {
+ const char *name2 = APR_ARRAY_IDX(md2->domains, i, const char*);
+ if (!md_contains(md1, name2, 0)) {
+ return 0;
+ }
+ }
+ return 1;
+ }
+ return 0;
+}
+
+md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name)
+{
+ int i;
+ for (i = 0; i < mds->nelts; ++i) {
+ md_t *md = APR_ARRAY_IDX(mds, i, md_t *);
+ if (!strcmp(name, md->name)) {
+ return md;
+ }
+ }
+ return NULL;
+}
+
+md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain)
+{
+ int i;
+ for (i = 0; i < mds->nelts; ++i) {
+ md_t *md = APR_ARRAY_IDX(mds, i, md_t *);
+ if (md_contains(md, domain, 0)) {
+ return md;
+ }
+ }
+ return NULL;
+}
+
+md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md)
+{
+ int i;
+ for (i = 0; i < mds->nelts; ++i) {
+ md_t *o = APR_ARRAY_IDX(mds, i, md_t *);
+ if (strcmp(o->name, md->name) && md_common_name(o, md)) {
+ return o;
+ }
+ }
+ return NULL;
+}
+
+int md_cert_count(const md_t *md)
+{
+ /* cert are defined as a list of static files or a list of private key specs */
+ if (md->cert_files && md->cert_files->nelts) {
+ return md->cert_files->nelts;
+ }
+ return md_pkeys_spec_count(md->pks);
+}
+
+md_t *md_create(apr_pool_t *p, apr_array_header_t *domains)
+{
+ md_t *md;
+
+ md = md_create_empty(p);
+ md->domains = md_array_str_compact(p, domains, 0);
+ md->name = APR_ARRAY_IDX(md->domains, 0, const char *);
+
+ return md;
+}
+
+/**************************************************************************************************/
+/* lifetime */
+
+md_t *md_copy(apr_pool_t *p, const md_t *src)
+{
+ md_t *md;
+
+ md = apr_pcalloc(p, sizeof(*md));
+ if (md) {
+ memcpy(md, src, sizeof(*md));
+ md->domains = apr_array_copy(p, src->domains);
+ md->contacts = apr_array_copy(p, src->contacts);
+ if (src->ca_challenges) {
+ md->ca_challenges = apr_array_copy(p, src->ca_challenges);
+ }
+ md->acme_tls_1_domains = apr_array_copy(p, src->acme_tls_1_domains);
+ md->pks = md_pkeys_spec_clone(p, src->pks);
+ }
+ return md;
+}
+
+md_t *md_clone(apr_pool_t *p, const md_t *src)
+{
+ md_t *md;
+
+ md = apr_pcalloc(p, sizeof(*md));
+ if (md) {
+ md->state = src->state;
+ md->name = apr_pstrdup(p, src->name);
+ md->require_https = src->require_https;
+ md->must_staple = src->must_staple;
+ md->renew_mode = src->renew_mode;
+ md->domains = md_array_str_compact(p, src->domains, 0);
+ md->pks = md_pkeys_spec_clone(p, src->pks);
+ md->renew_window = src->renew_window;
+ md->warn_window = src->warn_window;
+ md->contacts = md_array_str_clone(p, src->contacts);
+ if (src->ca_proto) md->ca_proto = apr_pstrdup(p, src->ca_proto);
+ if (src->ca_urls) {
+ md->ca_urls = md_array_str_clone(p, src->ca_urls);
+ }
+ if (src->ca_effective) md->ca_effective = apr_pstrdup(p, src->ca_effective);
+ if (src->ca_account) md->ca_account = apr_pstrdup(p, src->ca_account);
+ if (src->ca_agreement) md->ca_agreement = apr_pstrdup(p, src->ca_agreement);
+ if (src->defn_name) md->defn_name = apr_pstrdup(p, src->defn_name);
+ md->defn_line_number = src->defn_line_number;
+ if (src->ca_challenges) {
+ 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->dns01_cmd) md->dns01_cmd = apr_pstrdup(p, src->dns01_cmd);
+ if (src->cert_files) md->cert_files = md_array_str_clone(p, src->cert_files);
+ if (src->pkey_files) md->pkey_files = md_array_str_clone(p, src->pkey_files);
+ }
+ return md;
+}
+
+/**************************************************************************************************/
+/* format conversion */
+
+md_json_t *md_to_json(const md_t *md, apr_pool_t *p)
+{
+ md_json_t *json = md_json_create(p);
+ if (json) {
+ apr_array_header_t *domains = md_array_str_compact(p, md->domains, 0);
+ md_json_sets(md->name, json, MD_KEY_NAME, NULL);
+ md_json_setsa(domains, json, MD_KEY_DOMAINS, NULL);
+ md_json_setsa(md->contacts, json, MD_KEY_CONTACTS, NULL);
+ md_json_setl(md->transitive, json, MD_KEY_TRANSITIVE, NULL);
+ md_json_sets(md->ca_account, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL);
+ md_json_sets(md->ca_proto, json, MD_KEY_CA, MD_KEY_PROTO, NULL);
+ md_json_sets(md->ca_effective, json, MD_KEY_CA, MD_KEY_URL, NULL);
+ if (md->ca_urls && !apr_is_empty_array(md->ca_urls)) {
+ md_json_setsa(md->ca_urls, json, MD_KEY_CA, MD_KEY_URLS, NULL);
+ }
+ md_json_sets(md->ca_agreement, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL);
+ if (!md_pkeys_spec_is_empty(md->pks)) {
+ md_json_setj(md_pkeys_spec_to_json(md->pks, p), json, MD_KEY_PKEY, NULL);
+ }
+ md_json_setl(md->state, json, MD_KEY_STATE, NULL);
+ if (md->state_descr)
+ md_json_sets(md->state_descr, json, MD_KEY_STATE_DESCR, NULL);
+ md_json_setl(md->renew_mode, json, MD_KEY_RENEW_MODE, NULL);
+ if (md->renew_window)
+ md_json_sets(md_timeslice_format(md->renew_window, p), json, MD_KEY_RENEW_WINDOW, NULL);
+ if (md->warn_window)
+ md_json_sets(md_timeslice_format(md->warn_window, p), json, MD_KEY_WARN_WINDOW, NULL);
+ if (md->ca_challenges && md->ca_challenges->nelts > 0) {
+ apr_array_header_t *na;
+ na = md_array_str_compact(p, md->ca_challenges, 0);
+ md_json_setsa(na, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL);
+ }
+ switch (md->require_https) {
+ case MD_REQUIRE_TEMPORARY:
+ md_json_sets(MD_KEY_TEMPORARY, json, MD_KEY_REQUIRE_HTTPS, NULL);
+ break;
+ case MD_REQUIRE_PERMANENT:
+ md_json_sets(MD_KEY_PERMANENT, json, MD_KEY_REQUIRE_HTTPS, NULL);
+ break;
+ default:
+ break;
+ }
+ md_json_setb(md->must_staple > 0, json, MD_KEY_MUST_STAPLE, NULL);
+ md_json_setsa(md->acme_tls_1_domains, json, MD_KEY_PROTO, MD_KEY_ACME_TLS_1, NULL);
+ if (md->cert_files) md_json_setsa(md->cert_files, json, MD_KEY_CERT_FILES, NULL);
+ if (md->pkey_files) md_json_setsa(md->pkey_files, json, MD_KEY_PKEY_FILES, NULL);
+ md_json_setb(md->stapling > 0, json, MD_KEY_STAPLING, NULL);
+ if (md->dns01_cmd) md_json_sets(md->dns01_cmd, json, MD_KEY_CMD_DNS01, NULL);
+ if (md->ca_eab_kid && strcmp("none", md->ca_eab_kid)) {
+ md_json_sets(md->ca_eab_kid, json, MD_KEY_EAB, MD_KEY_KID, NULL);
+ if (md->ca_eab_hmac) md_json_sets(md->ca_eab_hmac, json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+ }
+ return json;
+ }
+ return NULL;
+}
+
+md_t *md_from_json(md_json_t *json, apr_pool_t *p)
+{
+ const char *s;
+ md_t *md = md_create_empty(p);
+ if (md) {
+ md->name = md_json_dups(p, json, MD_KEY_NAME, NULL);
+ md_json_dupsa(md->domains, p, json, MD_KEY_DOMAINS, NULL);
+ md_json_dupsa(md->contacts, p, json, MD_KEY_CONTACTS, NULL);
+ md->ca_account = md_json_dups(p, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL);
+ md->ca_proto = md_json_dups(p, json, MD_KEY_CA, MD_KEY_PROTO, NULL);
+ md->ca_effective = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL);
+ if (md_json_has_key(json, MD_KEY_CA, MD_KEY_URLS, NULL)) {
+ md->ca_urls = apr_array_make(p, 5, sizeof(const char*));
+ md_json_dupsa(md->ca_urls, p, json, MD_KEY_CA, MD_KEY_URLS, NULL);
+ }
+ else if (md->ca_effective) {
+ /* compat for old format where we had only a single url */
+ md->ca_urls = apr_array_make(p, 5, sizeof(const char*));
+ APR_ARRAY_PUSH(md->ca_urls, const char*) = md->ca_effective;
+ }
+ md->ca_agreement = md_json_dups(p, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL);
+ if (md_json_has_key(json, MD_KEY_PKEY, NULL)) {
+ md->pks = md_pkeys_spec_from_json(md_json_getj(json, MD_KEY_PKEY, NULL), p);
+ }
+ md->state = (md_state_t)md_json_getl(json, MD_KEY_STATE, NULL);
+ md->state_descr = md_json_dups(p, json, MD_KEY_STATE_DESCR, NULL);
+ if (MD_S_EXPIRED_DEPRECATED == md->state) md->state = MD_S_COMPLETE;
+ md->renew_mode = (int)md_json_getl(json, MD_KEY_RENEW_MODE, NULL);
+ md->domains = md_array_str_compact(p, md->domains, 0);
+ md->transitive = (int)md_json_getl(json, MD_KEY_TRANSITIVE, NULL);
+ s = md_json_gets(json, MD_KEY_RENEW_WINDOW, NULL);
+ md_timeslice_parse(&md->renew_window, p, s, MD_TIME_LIFE_NORM);
+ s = md_json_gets(json, MD_KEY_WARN_WINDOW, NULL);
+ md_timeslice_parse(&md->warn_window, p, s, MD_TIME_LIFE_NORM);
+ if (md_json_has_key(json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL)) {
+ md->ca_challenges = apr_array_make(p, 5, sizeof(const char*));
+ md_json_dupsa(md->ca_challenges, p, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL);
+ }
+ md->require_https = MD_REQUIRE_OFF;
+ s = md_json_gets(json, MD_KEY_REQUIRE_HTTPS, NULL);
+ if (s && !strcmp(MD_KEY_TEMPORARY, s)) {
+ md->require_https = MD_REQUIRE_TEMPORARY;
+ }
+ else if (s && !strcmp(MD_KEY_PERMANENT, s)) {
+ md->require_https = MD_REQUIRE_PERMANENT;
+ }
+ md->must_staple = (int)md_json_getb(json, MD_KEY_MUST_STAPLE, NULL);
+ md_json_dupsa(md->acme_tls_1_domains, p, json, MD_KEY_PROTO, MD_KEY_ACME_TLS_1, NULL);
+
+ if (md_json_has_key(json, MD_KEY_CERT_FILES, NULL)) {
+ md->cert_files = apr_array_make(p, 3, sizeof(char*));
+ md->pkey_files = apr_array_make(p, 3, sizeof(char*));
+ md_json_dupsa(md->cert_files, p, json, MD_KEY_CERT_FILES, NULL);
+ md_json_dupsa(md->pkey_files, p, json, MD_KEY_PKEY_FILES, NULL);
+ }
+ md->stapling = (int)md_json_getb(json, MD_KEY_STAPLING, NULL);
+ md->dns01_cmd = md_json_dups(p, json, MD_KEY_CMD_DNS01, NULL);
+ if (md_json_has_key(json, MD_KEY_EAB, NULL)) {
+ md->ca_eab_kid = md_json_dups(p, json, MD_KEY_EAB, MD_KEY_KID, NULL);
+ md->ca_eab_hmac = md_json_dups(p, json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+ }
+ return md;
+ }
+ return NULL;
+}
+
+md_json_t *md_to_public_json(const md_t *md, apr_pool_t *p)
+{
+ md_json_t *json = md_to_json(md, p);
+ if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) {
+ md_json_sets("***", json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+ }
+ return json;
+}
+
+typedef struct {
+ const char *name;
+ const char *url;
+} md_ca_t;
+
+#define LE_ACMEv2_PROD "https://acme-v02.api.letsencrypt.org/directory"
+#define LE_ACMEv2_STAGING "https://acme-staging-v02.api.letsencrypt.org/directory"
+#define BUYPASS_ACME "https://api.buypass.com/acme/directory"
+#define BUYPASS_ACME_TEST "https://api.test4.buypass.no/acme/directory"
+
+static md_ca_t KNOWN_CAs[] = {
+ { "LetsEncrypt", LE_ACMEv2_PROD },
+ { "LetsEncrypt-Test", LE_ACMEv2_STAGING },
+ { "Buypass", BUYPASS_ACME },
+ { "Buypass-Test", BUYPASS_ACME_TEST },
+};
+
+const char *md_get_ca_name_from_url(apr_pool_t *p, const char *url)
+{
+ apr_uri_t uri_parsed;
+ unsigned int i;
+
+ for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) {
+ if (!apr_strnatcasecmp(KNOWN_CAs[i].url, url)) {
+ return KNOWN_CAs[i].name;
+ }
+ }
+ if (APR_SUCCESS == apr_uri_parse(p, url, &uri_parsed)) {
+ return uri_parsed.hostname;
+ }
+ return apr_pstrdup(p, url);
+}
+
+apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const char *name)
+{
+ const char *err;
+ unsigned int i;
+ apr_status_t rv = APR_SUCCESS;
+
+ *purl = NULL;
+ for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) {
+ if (!apr_strnatcasecmp(KNOWN_CAs[i].name, name)) {
+ *purl = KNOWN_CAs[i].url;
+ goto leave;
+ }
+ }
+ *purl = name;
+ rv = md_util_abs_uri_check(p, name, &err);
+ if (APR_SUCCESS != rv) {
+ apr_array_header_t *names;
+
+ names = apr_array_make(p, 10, sizeof(const char*));
+ for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) {
+ APR_ARRAY_PUSH(names, const char *) = KNOWN_CAs[i].name;
+ }
+ *purl = apr_psprintf(p,
+ "The CA name '%s' is not known and it is not a URL either (%s). "
+ "Known CA names are: %s.",
+ name, err, apr_array_pstrcat(p, names, ' '));
+ }
+leave:
+ return rv;
+}
diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c
new file mode 100644
index 0000000..f2b0cd5
--- /dev/null
+++ b/modules/md/md_crypt.c
@@ -0,0 +1,2140 @@
+/* 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_file_io.h>
+#include <apr_strings.h>
+#include <httpd.h>
+#include <http_core.h>
+
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+#include <openssl/pem.h>
+#include <openssl/rand.h>
+#include <openssl/rsa.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_time.h"
+#include "md_util.h"
+
+/* getpid for *NIX */
+#if APR_HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#if APR_HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+/* getpid for Windows */
+#if APR_HAVE_PROCESS_H
+#include <process.h>
+#endif
+
+#if defined(LIBRESSL_VERSION_NUMBER)
+/* Missing from LibreSSL */
+#define MD_USE_OPENSSL_PRE_1_1_API (LIBRESSL_VERSION_NUMBER < 0x2070000f)
+#else /* defined(LIBRESSL_VERSION_NUMBER) */
+#define MD_USE_OPENSSL_PRE_1_1_API (OPENSSL_VERSION_NUMBER < 0x10100000L)
+#endif
+
+#if (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER < 0x3050000fL)) || (OPENSSL_VERSION_NUMBER < 0x10100000L)
+/* Missing from LibreSSL < 3.5.0 and only available since OpenSSL v1.1.x */
+#ifndef OPENSSL_NO_CT
+#define OPENSSL_NO_CT
+#endif
+#endif
+
+#ifndef OPENSSL_NO_CT
+#include <openssl/ct.h>
+#endif
+
+static int initialized;
+
+struct md_pkey_t {
+ apr_pool_t *pool;
+ EVP_PKEY *pkey;
+};
+
+#ifdef MD_HAVE_ARC4RANDOM
+
+static void seed_RAND(int pid)
+{
+ char seed[128];
+
+ (void)pid;
+ arc4random_buf(seed, sizeof(seed));
+ RAND_seed(seed, sizeof(seed));
+}
+
+#else /* ifdef MD_HAVE_ARC4RANDOM */
+
+static int rand_choosenum(int l, int h)
+{
+ int i;
+ char buf[50];
+
+ apr_snprintf(buf, sizeof(buf), "%.0f",
+ (((double)(rand()%RAND_MAX)/RAND_MAX)*(h-l)));
+ i = atoi(buf)+1;
+ if (i < l) i = l;
+ if (i > h) i = h;
+ return i;
+}
+
+static void seed_RAND(int pid)
+{
+ unsigned char stackdata[256];
+ /* stolen from mod_ssl/ssl_engine_rand.c */
+ int n;
+ struct {
+ time_t t;
+ pid_t pid;
+ } my_seed;
+
+ /*
+ * seed in the current time (usually just 4 bytes)
+ */
+ my_seed.t = time(NULL);
+
+ /*
+ * seed in the current process id (usually just 4 bytes)
+ */
+ my_seed.pid = pid;
+
+ RAND_seed((unsigned char *)&my_seed, sizeof(my_seed));
+
+ /*
+ * seed in some current state of the run-time stack (128 bytes)
+ */
+ n = rand_choosenum(0, sizeof(stackdata)-128-1);
+ RAND_seed(stackdata+n, 128);
+}
+
+#endif /*ifdef MD_HAVE_ARC4RANDOM (else part) */
+
+
+apr_status_t md_crypt_init(apr_pool_t *pool)
+{
+ (void)pool;
+
+ if (!initialized) {
+ int pid = getpid();
+
+ ERR_load_crypto_strings();
+ OpenSSL_add_all_algorithms();
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, pool, "initializing RAND");
+ while (!RAND_status()) {
+ seed_RAND(pid);
+ }
+
+ initialized = 1;
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t fwrite_buffer(void *baton, apr_file_t *f, apr_pool_t *p)
+{
+ md_data_t *buf = baton;
+ apr_size_t wlen;
+
+ (void)p;
+ return apr_file_write_full(f, buf->data, buf->len, &wlen);
+}
+
+apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p)
+{
+ apr_status_t rv;
+
+ if (len > INT_MAX) {
+ return APR_ENOTIMPL;
+ }
+ if (APR_SUCCESS == (rv = md_crypt_init(p))) {
+ RAND_bytes((unsigned char*)buf, (int)len);
+ }
+ return rv;
+}
+
+typedef struct {
+ const char *pass_phrase;
+ int pass_len;
+} passwd_ctx;
+
+static int pem_passwd(char *buf, int size, int rwflag, void *baton)
+{
+ passwd_ctx *ctx = baton;
+
+ (void)rwflag;
+ if (ctx->pass_len > 0) {
+ if (ctx->pass_len < size) {
+ size = (int)ctx->pass_len;
+ }
+ memcpy(buf, ctx->pass_phrase, (size_t)size);
+ } else {
+ return 0;
+ }
+ return size;
+}
+
+/**************************************************************************************************/
+/* date time things */
+
+/* Get the apr time (micro seconds, since 1970) from an ASN1 time, as stored in X509
+ * certificates. OpenSSL now has a utility function, but other *SSL derivatives have
+ * not caughts up yet or chose to ignore. An alternative is implemented, we prefer
+ * however the *SSL to maintain such things.
+ */
+static apr_time_t md_asn1_time_get(const ASN1_TIME* time)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10002000L || (defined(LIBRESSL_VERSION_NUMBER) && \
+ LIBRESSL_VERSION_NUMBER < 0x3060000fL)
+ /* courtesy: https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion#11263731
+ * all bugs are mine */
+ apr_time_exp_t t;
+ apr_time_t ts;
+ const char* str = (const char*) time->data;
+ apr_size_t i = 0;
+
+ memset(&t, 0, sizeof(t));
+
+ if (time->type == V_ASN1_UTCTIME) {/* two digit year */
+ t.tm_year = (str[i++] - '0') * 10;
+ t.tm_year += (str[i++] - '0');
+ if (t.tm_year < 70)
+ t.tm_year += 100;
+ }
+ else if (time->type == V_ASN1_GENERALIZEDTIME) {/* four digit year */
+ t.tm_year = (str[i++] - '0') * 1000;
+ t.tm_year+= (str[i++] - '0') * 100;
+ t.tm_year+= (str[i++] - '0') * 10;
+ t.tm_year+= (str[i++] - '0');
+ t.tm_year -= 1900;
+ }
+ t.tm_mon = (str[i++] - '0') * 10;
+ t.tm_mon += (str[i++] - '0') - 1; /* -1 since January is 0 not 1. */
+ t.tm_mday = (str[i++] - '0') * 10;
+ t.tm_mday+= (str[i++] - '0');
+ t.tm_hour = (str[i++] - '0') * 10;
+ t.tm_hour+= (str[i++] - '0');
+ t.tm_min = (str[i++] - '0') * 10;
+ t.tm_min += (str[i++] - '0');
+ t.tm_sec = (str[i++] - '0') * 10;
+ t.tm_sec += (str[i++] - '0');
+
+ if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) {
+ return ts;
+ }
+ return 0;
+#else
+ int secs, days;
+ apr_time_t ts = apr_time_now();
+
+ if (ASN1_TIME_diff(&days, &secs, NULL, time)) {
+ ts += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs);
+ }
+ return ts;
+#endif
+}
+
+apr_time_t md_asn1_generalized_time_get(void *ASN1_GENERALIZEDTIME)
+{
+ return md_asn1_time_get(ASN1_GENERALIZEDTIME);
+}
+
+/**************************************************************************************************/
+/* OID/NID things */
+
+static int get_nid(const char *num, const char *sname, const char *lname)
+{
+ /* Funny API, an OID for a feature might be configured or
+ * maybe not. In the second case, we need to add it. But adding
+ * when it already is there is an error... */
+ int nid = OBJ_txt2nid(num);
+ if (NID_undef == nid) {
+ nid = OBJ_create(num, sname, lname);
+ }
+ return nid;
+}
+
+#define MD_GET_NID(x) get_nid(MD_OID_##x##_NUM, MD_OID_##x##_SNAME, MD_OID_##x##_LNAME)
+
+/**************************************************************************************************/
+/* private keys */
+
+md_pkeys_spec_t *md_pkeys_spec_make(apr_pool_t *p)
+{
+ md_pkeys_spec_t *pks;
+
+ pks = apr_pcalloc(p, sizeof(*pks));
+ pks->p = p;
+ pks->specs = apr_array_make(p, 2, sizeof(md_pkey_spec_t*));
+ return pks;
+}
+
+void md_pkeys_spec_add(md_pkeys_spec_t *pks, md_pkey_spec_t *spec)
+{
+ APR_ARRAY_PUSH(pks->specs, md_pkey_spec_t*) = spec;
+}
+
+void md_pkeys_spec_add_default(md_pkeys_spec_t *pks)
+{
+ md_pkey_spec_t *spec;
+
+ spec = apr_pcalloc(pks->p, sizeof(*spec));
+ spec->type = MD_PKEY_TYPE_DEFAULT;
+ md_pkeys_spec_add(pks, spec);
+}
+
+int md_pkeys_spec_contains_rsa(md_pkeys_spec_t *pks)
+{
+ md_pkey_spec_t *spec;
+ int i;
+ for (i = 0; i < pks->specs->nelts; ++i) {
+ spec = APR_ARRAY_IDX(pks->specs, i, md_pkey_spec_t*);
+ if (MD_PKEY_TYPE_RSA == spec->type) return 1;
+ }
+ return 0;
+}
+
+void md_pkeys_spec_add_rsa(md_pkeys_spec_t *pks, unsigned int bits)
+{
+ md_pkey_spec_t *spec;
+
+ spec = apr_pcalloc(pks->p, sizeof(*spec));
+ spec->type = MD_PKEY_TYPE_RSA;
+ spec->params.rsa.bits = bits;
+ md_pkeys_spec_add(pks, spec);
+}
+
+int md_pkeys_spec_contains_ec(md_pkeys_spec_t *pks, const char *curve)
+{
+ md_pkey_spec_t *spec;
+ int i;
+ for (i = 0; i < pks->specs->nelts; ++i) {
+ spec = APR_ARRAY_IDX(pks->specs, i, md_pkey_spec_t*);
+ if (MD_PKEY_TYPE_EC == spec->type
+ && !apr_strnatcasecmp(curve, spec->params.ec.curve)) return 1;
+ }
+ return 0;
+}
+
+void md_pkeys_spec_add_ec(md_pkeys_spec_t *pks, const char *curve)
+{
+ md_pkey_spec_t *spec;
+
+ spec = apr_pcalloc(pks->p, sizeof(*spec));
+ spec->type = MD_PKEY_TYPE_EC;
+ spec->params.ec.curve = apr_pstrdup(pks->p, curve);
+ md_pkeys_spec_add(pks, spec);
+}
+
+md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p)
+{
+ md_json_t *json = md_json_create(p);
+ if (json) {
+ switch (spec->type) {
+ case MD_PKEY_TYPE_DEFAULT:
+ md_json_sets("Default", json, MD_KEY_TYPE, NULL);
+ break;
+ case MD_PKEY_TYPE_RSA:
+ md_json_sets("RSA", json, MD_KEY_TYPE, NULL);
+ if (spec->params.rsa.bits >= MD_PKEY_RSA_BITS_MIN) {
+ md_json_setl((long)spec->params.rsa.bits, json, MD_KEY_BITS, NULL);
+ }
+ break;
+ case MD_PKEY_TYPE_EC:
+ md_json_sets("EC", json, MD_KEY_TYPE, NULL);
+ if (spec->params.ec.curve) {
+ md_json_sets(spec->params.ec.curve, json, MD_KEY_CURVE, NULL);
+ }
+ break;
+ default:
+ md_json_sets("Unsupported", json, MD_KEY_TYPE, NULL);
+ break;
+ }
+ }
+ return json;
+}
+
+static apr_status_t spec_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+ md_json_t *jspec;
+
+ (void)baton;
+ jspec = md_pkey_spec_to_json((md_pkey_spec_t*)value, p);
+ return md_json_setj(jspec, json, NULL);
+}
+
+md_json_t *md_pkeys_spec_to_json(const md_pkeys_spec_t *pks, apr_pool_t *p)
+{
+ md_json_t *j;
+
+ if (pks->specs->nelts == 1) {
+ return md_pkey_spec_to_json(md_pkeys_spec_get(pks, 0), p);
+ }
+ j = md_json_create(p);
+ md_json_seta(pks->specs, spec_to_json, (void*)pks, j, "specs", NULL);
+ return md_json_getj(j, "specs", NULL);
+}
+
+md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p)
+{
+ md_pkey_spec_t *spec = apr_pcalloc(p, sizeof(*spec));
+ const char *s;
+ long l;
+
+ if (spec) {
+ s = md_json_gets(json, MD_KEY_TYPE, NULL);
+ if (!s || !apr_strnatcasecmp("Default", s)) {
+ spec->type = MD_PKEY_TYPE_DEFAULT;
+ }
+ else if (!apr_strnatcasecmp("RSA", s)) {
+ spec->type = MD_PKEY_TYPE_RSA;
+ l = md_json_getl(json, MD_KEY_BITS, NULL);
+ if (l >= MD_PKEY_RSA_BITS_MIN) {
+ spec->params.rsa.bits = (unsigned int)l;
+ }
+ else {
+ spec->params.rsa.bits = MD_PKEY_RSA_BITS_DEF;
+ }
+ }
+ else if (!apr_strnatcasecmp("EC", s)) {
+ spec->type = MD_PKEY_TYPE_EC;
+ s = md_json_gets(json, MD_KEY_CURVE, NULL);
+ if (s) {
+ spec->params.ec.curve = apr_pstrdup(p, s);
+ }
+ else {
+ spec->params.ec.curve = NULL;
+ }
+ }
+ }
+ return spec;
+}
+
+static apr_status_t spec_from_json(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton)
+{
+ (void)baton;
+ *pvalue = md_pkey_spec_from_json(json, p);
+ return APR_SUCCESS;
+}
+
+md_pkeys_spec_t *md_pkeys_spec_from_json(struct md_json_t *json, apr_pool_t *p)
+{
+ md_pkeys_spec_t *pks;
+ md_pkey_spec_t *spec;
+
+ pks = md_pkeys_spec_make(p);
+ if (md_json_is(MD_JSON_TYPE_ARRAY, json, NULL)) {
+ md_json_geta(pks->specs, spec_from_json, pks, json, NULL);
+ }
+ else {
+ spec = md_pkey_spec_from_json(json, p);
+ md_pkeys_spec_add(pks, spec);
+ }
+ return pks;
+}
+
+static int pkey_spec_eq(md_pkey_spec_t *s1, md_pkey_spec_t *s2)
+{
+ if (s1 == s2) {
+ return 1;
+ }
+ if (s1 && s2 && s1->type == s2->type) {
+ switch (s1->type) {
+ case MD_PKEY_TYPE_DEFAULT:
+ return 1;
+ case MD_PKEY_TYPE_RSA:
+ if (s1->params.rsa.bits == s2->params.rsa.bits) {
+ return 1;
+ }
+ break;
+ case MD_PKEY_TYPE_EC:
+ if (s1->params.ec.curve == s2->params.ec.curve) {
+ return 1;
+ }
+ else if (!s1->params.ec.curve || !s2->params.ec.curve) {
+ return 0;
+ }
+ return !strcmp(s1->params.ec.curve, s2->params.ec.curve);
+ }
+ }
+ return 0;
+}
+
+int md_pkeys_spec_eq(md_pkeys_spec_t *pks1, md_pkeys_spec_t *pks2)
+{
+ int i;
+
+ if (pks1 == pks2) {
+ return 1;
+ }
+ if (pks1 && pks2 && pks1->specs->nelts == pks2->specs->nelts) {
+ for(i = 0; i < pks1->specs->nelts; ++i) {
+ if (!pkey_spec_eq(APR_ARRAY_IDX(pks1->specs, i, md_pkey_spec_t *),
+ APR_ARRAY_IDX(pks2->specs, i, md_pkey_spec_t *))) {
+ return 0;
+ }
+ }
+ return 1;
+ }
+ return 0;
+}
+
+static md_pkey_spec_t *pkey_spec_clone(apr_pool_t *p, md_pkey_spec_t *spec)
+{
+ md_pkey_spec_t *nspec;
+
+ nspec = apr_pcalloc(p, sizeof(*nspec));
+ nspec->type = spec->type;
+ switch (spec->type) {
+ case MD_PKEY_TYPE_DEFAULT:
+ break;
+ case MD_PKEY_TYPE_RSA:
+ nspec->params.rsa.bits = spec->params.rsa.bits;
+ break;
+ case MD_PKEY_TYPE_EC:
+ nspec->params.ec.curve = apr_pstrdup(p, spec->params.ec.curve);
+ break;
+ }
+ return nspec;
+}
+
+const char *md_pkey_spec_name(const md_pkey_spec_t *spec)
+{
+ if (!spec) return "rsa";
+ switch (spec->type) {
+ case MD_PKEY_TYPE_DEFAULT:
+ case MD_PKEY_TYPE_RSA:
+ return "rsa";
+ case MD_PKEY_TYPE_EC:
+ return spec->params.ec.curve;
+ }
+ return "unknown";
+}
+
+int md_pkeys_spec_is_empty(const md_pkeys_spec_t *pks)
+{
+ return NULL == pks || 0 == pks->specs->nelts;
+}
+
+md_pkeys_spec_t *md_pkeys_spec_clone(apr_pool_t *p, const md_pkeys_spec_t *pks)
+{
+ md_pkeys_spec_t *npks = NULL;
+ md_pkey_spec_t *spec;
+ int i;
+
+ if (pks && pks->specs->nelts > 0) {
+ npks = apr_pcalloc(p, sizeof(*npks));
+ npks->specs = apr_array_make(p, pks->specs->nelts, sizeof(md_pkey_spec_t*));
+ for (i = 0; i < pks->specs->nelts; ++i) {
+ spec = APR_ARRAY_IDX(pks->specs, i, md_pkey_spec_t*);
+ APR_ARRAY_PUSH(npks->specs, md_pkey_spec_t*) = pkey_spec_clone(p, spec);
+ }
+ }
+ return npks;
+}
+
+int md_pkeys_spec_count(const md_pkeys_spec_t *pks)
+{
+ return md_pkeys_spec_is_empty(pks)? 1 : pks->specs->nelts;
+}
+
+static md_pkey_spec_t PkeySpecDef = { MD_PKEY_TYPE_DEFAULT, {{ 0 }} };
+
+md_pkey_spec_t *md_pkeys_spec_get(const md_pkeys_spec_t *pks, int index)
+{
+ if (md_pkeys_spec_is_empty(pks)) {
+ return index == 1? &PkeySpecDef : NULL;
+ }
+ else if (pks && index >= 0 && index < pks->specs->nelts) {
+ return APR_ARRAY_IDX(pks->specs, index, md_pkey_spec_t*);
+ }
+ return NULL;
+}
+
+static md_pkey_t *make_pkey(apr_pool_t *p)
+{
+ md_pkey_t *pkey = apr_pcalloc(p, sizeof(*pkey));
+ pkey->pool = p;
+ return pkey;
+}
+
+static apr_status_t pkey_cleanup(void *data)
+{
+ md_pkey_t *pkey = data;
+ if (pkey->pkey) {
+ EVP_PKEY_free(pkey->pkey);
+ pkey->pkey = NULL;
+ }
+ return APR_SUCCESS;
+}
+
+void md_pkey_free(md_pkey_t *pkey)
+{
+ pkey_cleanup(pkey);
+}
+
+void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey)
+{
+ return pkey->pkey;
+}
+
+apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p,
+ const char *key, apr_size_t key_len,
+ const char *fname)
+{
+ apr_status_t rv = APR_ENOENT;
+ md_pkey_t *pkey;
+ BIO *bf;
+ passwd_ctx ctx;
+
+ pkey = make_pkey(p);
+ if (NULL != (bf = BIO_new_file(fname, "r"))) {
+ ctx.pass_phrase = key;
+ ctx.pass_len = (int)key_len;
+
+ ERR_clear_error();
+ pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, pem_passwd, &ctx);
+ BIO_free(bf);
+
+ if (pkey->pkey != NULL) {
+ rv = APR_SUCCESS;
+ apr_pool_cleanup_register(p, pkey, pkey_cleanup, apr_pool_cleanup_null);
+ }
+ else {
+ unsigned long err = ERR_get_error();
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p,
+ "error loading pkey %s: %s (pass phrase was %snull)", fname,
+ ERR_error_string(err, NULL), key? "not " : "");
+ }
+ }
+ *ppkey = (APR_SUCCESS == rv)? pkey : NULL;
+ return rv;
+}
+
+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());
+ const EVP_CIPHER *cipher = NULL;
+ pem_password_cb *cb = NULL;
+ void *cb_baton = NULL;
+ apr_status_t rv = APR_SUCCESS;
+ passwd_ctx ctx;
+ unsigned long err;
+ int i;
+
+ if (!bio) {
+ return APR_ENOMEM;
+ }
+ if (pass_len > INT_MAX) {
+ rv = APR_EINVAL;
+ goto cleanup;
+ }
+ if (pass && pass_len > 0) {
+ ctx.pass_phrase = pass;
+ ctx.pass_len = (int)pass_len;
+ cb = pem_passwd;
+ cb_baton = &ctx;
+ cipher = EVP_aes_256_cbc();
+ if (!cipher) {
+ rv = APR_ENOTIMPL;
+ goto cleanup;
+ }
+ }
+
+ ERR_clear_error();
+#if 1
+ if (!PEM_write_bio_PKCS8PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) {
+#else
+ if (!PEM_write_bio_PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) {
+#endif
+ err = ERR_get_error();
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "PEM_write key: %ld %s",
+ err, ERR_error_string(err, NULL));
+ rv = APR_EINVAL;
+ goto cleanup;
+ }
+
+ md_data_null(buf);
+ i = BIO_pending(bio);
+ if (i > 0) {
+ buf->data = apr_palloc(p, (apr_size_t)i);
+ i = BIO_read(bio, (char*)buf->data, i);
+ buf->len = (apr_size_t)i;
+ }
+
+cleanup:
+ BIO_free(bio);
+ return rv;
+}
+
+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_t buffer;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = pkey_to_buffer(&buffer, pkey, p, pass_phrase, pass_len))) {
+ return md_util_freplace(fname, perms, p, fwrite_buffer, &buffer);
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "save pkey %s (%s pass phrase, len=%d)",
+ fname, pass_len > 0? "with" : "without", (int)pass_len);
+ return rv;
+}
+
+apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool,
+ const struct md_http_response_t *res)
+{
+ apr_status_t rv;
+ apr_off_t data_len;
+ char *pem_data;
+ apr_size_t pem_len;
+ md_pkey_t *pkey;
+ BIO *bf;
+ passwd_ctx ctx;
+
+ rv = apr_brigade_length(res->body, 1, &data_len);
+ if (APR_SUCCESS != rv) goto leave;
+ if (data_len > 1024*1024) { /* certs usually are <2k each */
+ rv = APR_EINVAL;
+ goto leave;
+ }
+ rv = apr_brigade_pflatten(res->body, &pem_data, &pem_len, res->req->pool);
+ if (APR_SUCCESS != rv) goto leave;
+
+ if (NULL == (bf = BIO_new_mem_buf(pem_data, (int)pem_len))) {
+ rv = APR_ENOMEM;
+ goto leave;
+ }
+ pkey = make_pkey(pool);
+ ctx.pass_phrase = NULL;
+ ctx.pass_len = 0;
+ ERR_clear_error();
+ pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, NULL, &ctx);
+ BIO_free(bf);
+
+ if (pkey->pkey == NULL) {
+ unsigned long err = ERR_get_error();
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, pool,
+ "error loading pkey from http response: %s",
+ ERR_error_string(err, NULL));
+ goto leave;
+ }
+ rv = APR_SUCCESS;
+ apr_pool_cleanup_register(pool, pkey, pkey_cleanup, apr_pool_cleanup_null);
+
+leave:
+ *ppkey = (APR_SUCCESS == rv)? pkey : NULL;
+ return rv;
+}
+
+/* Determine the message digest used for signing with the given private key.
+ */
+static const EVP_MD *pkey_get_MD(md_pkey_t *pkey)
+{
+ switch (EVP_PKEY_id(pkey->pkey)) {
+#ifdef NID_ED25519
+ case NID_ED25519:
+ return NULL;
+#endif
+#ifdef NID_ED448
+ case NID_ED448:
+ return NULL;
+#endif
+ default:
+ return EVP_sha256();
+ }
+}
+
+static apr_status_t gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, unsigned int bits)
+{
+ EVP_PKEY_CTX *ctx = NULL;
+ apr_status_t rv;
+
+ *ppkey = make_pkey(p);
+ ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+ if (ctx
+ && EVP_PKEY_keygen_init(ctx) >= 0
+ && EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, (int)bits) >= 0
+ && EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) >= 0) {
+ rv = APR_SUCCESS;
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "error generate pkey RSA %d", bits);
+ *ppkey = NULL;
+ rv = APR_EGENERAL;
+ }
+
+ if (ctx != NULL) {
+ EVP_PKEY_CTX_free(ctx);
+ }
+ return rv;
+}
+
+static apr_status_t check_EC_curve(int nid, apr_pool_t *p) {
+ EC_builtin_curve *curves = NULL;
+ size_t nc, i;
+ int rv = APR_ENOENT;
+
+ nc = EC_get_builtin_curves(NULL, 0);
+ if (NULL == (curves = OPENSSL_malloc(sizeof(*curves) * nc)) ||
+ nc != EC_get_builtin_curves(curves, nc)) {
+ rv = APR_EGENERAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p,
+ "error looking up OpenSSL builtin EC curves");
+ goto leave;
+ }
+ for (i = 0; i < nc; ++i) {
+ if (nid == curves[i].nid) {
+ rv = APR_SUCCESS;
+ break;
+ }
+ }
+leave:
+ OPENSSL_free(curves);
+ return rv;
+}
+
+static apr_status_t gen_ec(md_pkey_t **ppkey, apr_pool_t *p, const char *curve)
+{
+ EVP_PKEY_CTX *ctx = NULL;
+ apr_status_t rv;
+ int curve_nid = NID_undef;
+
+ /* 1. Convert the cure into its registered identifier. Curves can be known under
+ * different names.
+ * 2. Determine, if the curve is supported by OpenSSL (or whatever is linked).
+ * 3. Generate the key, respecting the specific quirks some curves require.
+ */
+ curve_nid = EC_curve_nist2nid(curve);
+ /* In case this fails, try some names from other standards, like SECG */
+#ifdef NID_secp384r1
+ if (NID_undef == curve_nid && !apr_strnatcasecmp("secp384r1", curve)) {
+ curve_nid = NID_secp384r1;
+ curve = EC_curve_nid2nist(curve_nid);
+ }
+#endif
+#ifdef NID_X9_62_prime256v1
+ if (NID_undef == curve_nid && !apr_strnatcasecmp("secp256r1", curve)) {
+ curve_nid = NID_X9_62_prime256v1;
+ curve = EC_curve_nid2nist(curve_nid);
+ }
+#endif
+#ifdef NID_X9_62_prime192v1
+ if (NID_undef == curve_nid && !apr_strnatcasecmp("secp192r1", curve)) {
+ curve_nid = NID_X9_62_prime192v1;
+ curve = EC_curve_nid2nist(curve_nid);
+ }
+#endif
+#if defined(NID_X25519) && (!defined(LIBRESSL_VERSION_NUMBER) || \
+ LIBRESSL_VERSION_NUMBER >= 0x3070000fL)
+ if (NID_undef == curve_nid && !apr_strnatcasecmp("X25519", curve)) {
+ curve_nid = NID_X25519;
+ curve = EC_curve_nid2nist(curve_nid);
+ }
+#endif
+ if (NID_undef == curve_nid) {
+ /* OpenSSL object/curve names */
+ curve_nid = OBJ_sn2nid(curve);
+ }
+ if (NID_undef == curve_nid) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "ec curve unknown: %s", curve);
+ rv = APR_ENOTIMPL; goto leave;
+ }
+
+ *ppkey = make_pkey(p);
+ switch (curve_nid) {
+
+#if defined(NID_X25519) && (!defined(LIBRESSL_VERSION_NUMBER) || \
+ LIBRESSL_VERSION_NUMBER >= 0x3070000fL)
+ case NID_X25519:
+ /* no parameters */
+ if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL))
+ || EVP_PKEY_keygen_init(ctx) <= 0
+ || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p,
+ "error generate EC key for group: %s", curve);
+ rv = APR_EGENERAL; goto leave;
+ }
+ rv = APR_SUCCESS;
+ break;
+#endif
+
+#if defined(NID_X448) && !defined(LIBRESSL_VERSION_NUMBER)
+ case NID_X448:
+ /* no parameters */
+ if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X448, NULL))
+ || EVP_PKEY_keygen_init(ctx) <= 0
+ || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p,
+ "error generate EC key for group: %s", curve);
+ rv = APR_EGENERAL; goto leave;
+ }
+ rv = APR_SUCCESS;
+ break;
+#endif
+
+ default:
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
+ if (APR_SUCCESS != (rv = check_EC_curve(curve_nid, p))) goto leave;
+ if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))
+ || EVP_PKEY_paramgen_init(ctx) <= 0
+ || EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, curve_nid) <= 0
+ || EVP_PKEY_CTX_set_ec_param_enc(ctx, OPENSSL_EC_NAMED_CURVE) <= 0
+ || EVP_PKEY_keygen_init(ctx) <= 0
+ || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p,
+ "error generate EC key for group: %s", curve);
+ rv = APR_EGENERAL; goto leave;
+ }
+#else
+ if (APR_SUCCESS != (rv = check_EC_curve(curve_nid, p))) goto leave;
+ if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))
+ || EVP_PKEY_keygen_init(ctx) <= 0
+ || EVP_PKEY_CTX_ctrl_str(ctx, "ec_paramgen_curve", curve) <= 0
+ || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p,
+ "error generate EC key for group: %s", curve);
+ rv = APR_EGENERAL; goto leave;
+ }
+#endif
+ rv = APR_SUCCESS;
+ break;
+ }
+
+leave:
+ if (APR_SUCCESS != rv) *ppkey = NULL;
+ EVP_PKEY_CTX_free(ctx);
+ return rv;
+}
+
+apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec)
+{
+ md_pkey_type_t ptype = spec? spec->type : MD_PKEY_TYPE_DEFAULT;
+ switch (ptype) {
+ case MD_PKEY_TYPE_DEFAULT:
+ return gen_rsa(ppkey, p, MD_PKEY_RSA_BITS_DEF);
+ case MD_PKEY_TYPE_RSA:
+ return gen_rsa(ppkey, p, spec->params.rsa.bits);
+ case MD_PKEY_TYPE_EC:
+ return gen_ec(ppkey, p, spec->params.ec.curve);
+ default:
+ return APR_ENOTIMPL;
+ }
+}
+
+#if MD_USE_OPENSSL_PRE_1_1_API || (defined(LIBRESSL_VERSION_NUMBER) && \
+ LIBRESSL_VERSION_NUMBER < 0x2070000f)
+
+#ifndef NID_tlsfeature
+#define NID_tlsfeature 1020
+#endif
+
+static void RSA_get0_key(const RSA *r,
+ const BIGNUM **n, const BIGNUM **e, const BIGNUM **d)
+{
+ if (n != NULL)
+ *n = r->n;
+ if (e != NULL)
+ *e = r->e;
+ if (d != NULL)
+ *d = r->d;
+}
+
+#endif
+
+static const char *bn64(const BIGNUM *b, apr_pool_t *p)
+{
+ if (b) {
+ md_data_t buffer;
+
+ md_data_pinit(&buffer, (apr_size_t)BN_num_bytes(b), p);
+ if (buffer.data) {
+ BN_bn2bin(b, (unsigned char *)buffer.data);
+ return md_util_base64url_encode(&buffer, p);
+ }
+ }
+ return NULL;
+}
+
+const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p)
+{
+ const BIGNUM *e;
+ RSA *rsa = EVP_PKEY_get1_RSA(pkey->pkey);
+
+ if (!rsa) {
+ return NULL;
+ }
+ RSA_get0_key(rsa, NULL, &e, NULL);
+ return bn64(e, p);
+}
+
+const char *md_pkey_get_rsa_n64(md_pkey_t *pkey, apr_pool_t *p)
+{
+ const BIGNUM *n;
+ RSA *rsa = EVP_PKEY_get1_RSA(pkey->pkey);
+
+ if (!rsa) {
+ return NULL;
+ }
+ RSA_get0_key(rsa, &n, NULL, NULL);
+ return bn64(n, p);
+}
+
+apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *p,
+ const char *d, size_t dlen)
+{
+ EVP_MD_CTX *ctx = NULL;
+ md_data_t buffer;
+ unsigned int blen;
+ const char *sign64 = NULL;
+ apr_status_t rv = APR_ENOMEM;
+
+ md_data_pinit(&buffer, (apr_size_t)EVP_PKEY_size(pkey->pkey), p);
+ 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.data, &blen, pkey->pkey)) {
+ buffer.len = blen;
+ sign64 = md_util_base64url_encode(&buffer, p);
+ if (sign64) {
+ rv = APR_SUCCESS;
+ }
+ }
+ }
+ }
+ }
+
+ if (ctx) {
+ EVP_MD_CTX_destroy(ctx);
+ }
+ }
+
+ if (rv != APR_SUCCESS) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "signing");
+ }
+
+ *psign64 = sign64;
+ return rv;
+}
+
+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_t *digest;
+ apr_status_t rv = APR_ENOMEM;
+ unsigned int dlen;
+
+ digest = md_data_pmake(EVP_MAX_MD_SIZE, p);
+ ctx = EVP_MD_CTX_create();
+ if (ctx) {
+ rv = APR_ENOTIMPL;
+ if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) {
+ rv = APR_EGENERAL;
+ if (EVP_DigestUpdate(ctx, (unsigned char*)buf->data, buf->len)) {
+ if (EVP_DigestFinal(ctx, (unsigned char*)digest->data, &dlen)) {
+ digest->len = dlen;
+ rv = APR_SUCCESS;
+ }
+ }
+ }
+ }
+ if (ctx) {
+ EVP_MD_CTX_destroy(ctx);
+ }
+ *pdigest = (APR_SUCCESS == rv)? digest : NULL;
+ return rv;
+}
+
+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_t *digest;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = sha256_digest(&digest, p, d))) {
+ if (NULL == (digest64 = md_util_base64url_encode(digest, p))) {
+ rv = APR_EGENERAL;
+ }
+ }
+ *pdigest64 = digest64;
+ return rv;
+}
+
+apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p,
+ const md_data_t *data)
+{
+ md_data_t *digest;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = sha256_digest(&digest, p, data))) {
+ return md_data_to_hex(pdigesthex, 0, p, digest);
+ }
+ *pdigesthex = NULL;
+ return rv;
+}
+
+apr_status_t md_crypt_hmac64(const char **pmac64, const md_data_t *hmac_key,
+ apr_pool_t *p, const char *d, size_t dlen)
+{
+ const char *mac64 = NULL;
+ unsigned char *s;
+ unsigned int digest_len = 0;
+ md_data_t *digest;
+ apr_status_t rv = APR_SUCCESS;
+
+ digest = md_data_pmake(EVP_MAX_MD_SIZE, p);
+ s = HMAC(EVP_sha256(), (const unsigned char*)hmac_key->data, (int)hmac_key->len,
+ (const unsigned char*)d, (size_t)dlen,
+ (unsigned char*)digest->data, &digest_len);
+ if (!s) {
+ rv = APR_EINVAL;
+ goto cleanup;
+ }
+ digest->len = digest_len;
+ mac64 = md_util_base64url_encode(digest, p);
+
+cleanup:
+ *pmac64 = (APR_SUCCESS == rv)? mac64 : NULL;
+ return rv;
+}
+
+/**************************************************************************************************/
+/* certificates */
+
+struct md_cert_t {
+ apr_pool_t *pool;
+ X509 *x509;
+ apr_array_header_t *alt_names;
+};
+
+static apr_status_t cert_cleanup(void *data)
+{
+ md_cert_t *cert = data;
+ if (cert->x509) {
+ X509_free(cert->x509);
+ cert->x509 = NULL;
+ }
+ return APR_SUCCESS;
+}
+
+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;
+ return cert;
+}
+
+md_cert_t *md_cert_make(apr_pool_t *p, void *x509)
+{
+ 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)
+{
+ return cert->x509;
+}
+
+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) {
+ bn = ASN1_INTEGER_to_BN(ai, NULL);
+ serial = BN_bn2hex(bn);
+ s = apr_pstrdup(p, serial);
+ OPENSSL_free((void*)serial);
+ OPENSSL_free((void*)bn);
+ }
+ return s;
+}
+
+int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b)
+{
+ return X509_cmp(a->x509, b->x509) == 0;
+}
+
+int md_cert_is_valid_now(const md_cert_t *cert)
+{
+ return ((X509_cmp_current_time(X509_get_notBefore(cert->x509)) < 0)
+ && (X509_cmp_current_time(X509_get_notAfter(cert->x509)) > 0));
+}
+
+int md_cert_has_expired(const md_cert_t *cert)
+{
+ return (X509_cmp_current_time(X509_get_notAfter(cert->x509)) <= 0);
+}
+
+apr_time_t md_cert_get_not_after(const md_cert_t *cert)
+{
+ return md_asn1_time_get(X509_get_notAfter(cert->x509));
+}
+
+apr_time_t md_cert_get_not_before(const md_cert_t *cert)
+{
+ return md_asn1_time_get(X509_get_notBefore(cert->x509));
+}
+
+md_timeperiod_t md_cert_get_valid(const md_cert_t *cert)
+{
+ md_timeperiod_t p;
+ p.start = md_cert_get_not_before(cert);
+ p.end = md_cert_get_not_after(cert);
+ return p;
+}
+
+int md_cert_covers_domain(md_cert_t *cert, const char *domain_name)
+{
+ apr_array_header_t *alt_names;
+
+ md_cert_get_alt_names(&alt_names, cert, cert->pool);
+ if (alt_names) {
+ return md_array_str_index(alt_names, domain_name, 0, 0) >= 0;
+ }
+ return 0;
+}
+
+int md_cert_covers_md(md_cert_t *cert, const md_t *md)
+{
+ const char *name;
+ int i;
+
+ if (!cert->alt_names) {
+ md_cert_get_alt_names(&cert->alt_names, cert, cert->pool);
+ }
+ if (cert->alt_names) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, cert->pool, "cert has %d alt names",
+ cert->alt_names->nelts);
+ for (i = 0; i < md->domains->nelts; ++i) {
+ name = APR_ARRAY_IDX(md->domains, i, const char *);
+ if (!md_dns_domains_match(cert->alt_names, name)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, cert->pool,
+ "md domain %s not covered by cert", name);
+ return 0;
+ }
+ }
+ return 1;
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, cert->pool, "cert has NO alt names");
+ }
+ return 0;
+}
+
+apr_status_t md_cert_get_issuers_uri(const char **puri, const md_cert_t *cert, apr_pool_t *p)
+{
+ apr_status_t rv = APR_ENOENT;
+ STACK_OF(ACCESS_DESCRIPTION) *xinfos;
+ const char *uri = NULL;
+ unsigned char *buf;
+ int i;
+
+ xinfos = X509_get_ext_d2i(cert->x509, NID_info_access, NULL, NULL);
+ if (xinfos) {
+ for (i = 0; i < sk_ACCESS_DESCRIPTION_num(xinfos); i++) {
+ ACCESS_DESCRIPTION *val = sk_ACCESS_DESCRIPTION_value(xinfos, i);
+ if (OBJ_obj2nid(val->method) == NID_ad_ca_issuers
+ && val->location && val->location->type == GEN_URI) {
+ ASN1_STRING_to_UTF8(&buf, val->location->d.uniformResourceIdentifier);
+ uri = apr_pstrdup(p, (char *)buf);
+ OPENSSL_free(buf);
+ rv = APR_SUCCESS;
+ break;
+ }
+ }
+ sk_ACCESS_DESCRIPTION_pop_free(xinfos, ACCESS_DESCRIPTION_free);
+ }
+ *puri = (APR_SUCCESS == rv)? uri : NULL;
+ return rv;
+}
+
+apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, const md_cert_t *cert, apr_pool_t *p)
+{
+ apr_array_header_t *names;
+ apr_status_t rv = APR_ENOENT;
+ STACK_OF(GENERAL_NAME) *xalt_names;
+ unsigned char *buf;
+ int i;
+
+ xalt_names = X509_get_ext_d2i(cert->x509, NID_subject_alt_name, NULL, NULL);
+ if (xalt_names) {
+ GENERAL_NAME *cval;
+ const unsigned char *ip;
+ int len;
+
+ names = apr_array_make(p, sk_GENERAL_NAME_num(xalt_names), sizeof(char *));
+ for (i = 0; i < sk_GENERAL_NAME_num(xalt_names); ++i) {
+ cval = sk_GENERAL_NAME_value(xalt_names, i);
+ switch (cval->type) {
+ case GEN_DNS:
+ case GEN_URI:
+ ASN1_STRING_to_UTF8(&buf, cval->d.ia5);
+ APR_ARRAY_PUSH(names, const char *) = apr_pstrdup(p, (char*)buf);
+ OPENSSL_free(buf);
+ break;
+ case GEN_IPADD:
+ len = ASN1_STRING_length(cval->d.iPAddress);
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+ ip = ASN1_STRING_data(cval->d.iPAddress);
+#else
+ ip = ASN1_STRING_get0_data(cval->d.iPAddress);
+#endif
+ if (len == 4) /* IPv4 address */
+ APR_ARRAY_PUSH(names, const char *) = apr_psprintf(p, "%u.%u.%u.%u",
+ ip[0], ip[1], ip[2], ip[3]);
+ else if (len == 16) /* IPv6 address */
+ APR_ARRAY_PUSH(names, const char *) = apr_psprintf(p, "%02x%02x%02x%02x:"
+ "%02x%02x%02x%02x:"
+ "%02x%02x%02x%02x:"
+ "%02x%02x%02x%02x",
+ ip[0], ip[1], ip[2], ip[3],
+ ip[4], ip[5], ip[6], ip[7],
+ ip[8], ip[9], ip[10], ip[11],
+ ip[12], ip[13], ip[14], ip[15]);
+ else {
+ ; /* Unknown address type - Log? Assert? */
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ sk_GENERAL_NAME_pop_free(xalt_names, GENERAL_NAME_free);
+ rv = APR_SUCCESS;
+ }
+ *pnames = (APR_SUCCESS == rv)? names : NULL;
+ return rv;
+}
+
+apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname)
+{
+ FILE *f;
+ apr_status_t rv;
+ md_cert_t *cert;
+ X509 *x509;
+
+ rv = md_util_fopen(&f, fname, "r");
+ if (rv == APR_SUCCESS) {
+
+ x509 = PEM_read_X509(f, NULL, NULL, NULL);
+ rv = fclose(f);
+ if (x509 != NULL) {
+ cert = md_cert_make(p, x509);
+ }
+ else {
+ rv = APR_EINVAL;
+ }
+ }
+
+ *pcert = (APR_SUCCESS == rv)? cert : NULL;
+ return rv;
+}
+
+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;
+
+ if (!bio) {
+ return APR_ENOMEM;
+ }
+
+ ERR_clear_error();
+ PEM_write_bio_X509(bio, cert->x509);
+ if (ERR_get_error() > 0) {
+ BIO_free(bio);
+ return APR_EINVAL;
+ }
+
+ i = BIO_pending(bio);
+ if (i > 0) {
+ buffer->data = apr_palloc(p, (apr_size_t)i);
+ i = BIO_read(bio, (char*)buffer->data, i);
+ buffer->len = (apr_size_t)i;
+ }
+ BIO_free(bio);
+ return APR_SUCCESS;
+}
+
+apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p,
+ const char *fname, apr_fileperms_t perms)
+{
+ md_data_t buffer;
+ apr_status_t rv;
+
+ md_data_null(&buffer);
+ if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) {
+ return md_util_freplace(fname, perms, p, fwrite_buffer, &buffer);
+ }
+ return rv;
+}
+
+apr_status_t md_cert_to_base64url(const char **ps64, const md_cert_t *cert, apr_pool_t *p)
+{
+ md_data_t buffer;
+ apr_status_t rv;
+
+ md_data_null(&buffer);
+ if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, 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_t **pdigest, const md_cert_t *cert, apr_pool_t *p)
+{
+ md_data_t *digest;
+ unsigned int dlen;
+
+ digest = md_data_pmake(EVP_MAX_MD_SIZE, p);
+ X509_digest(cert->x509, EVP_sha256(), (unsigned char*)digest->data, &dlen);
+ digest->len = dlen;
+
+ *pdigest = digest;
+ return APR_SUCCESS;
+}
+
+apr_status_t md_cert_to_sha256_fingerprint(const char **pfinger, const md_cert_t *cert, apr_pool_t *p)
+{
+ md_data_t *digest;
+ apr_status_t rv;
+
+ rv = md_cert_to_sha256_digest(&digest, cert, p);
+ if (APR_SUCCESS == rv) {
+ return md_data_to_hex(pfinger, 0, p, digest);
+ }
+ *pfinger = NULL;
+ return rv;
+}
+
+static int md_cert_read_pem(BIO *bf, apr_pool_t *p, md_cert_t **pcert)
+{
+ md_cert_t *cert;
+ X509 *x509;
+ apr_status_t rv = APR_ENOENT;
+
+ ERR_clear_error();
+ x509 = PEM_read_bio_X509(bf, NULL, NULL, NULL);
+ if (x509 == NULL) goto cleanup;
+ cert = md_cert_make(p, x509);
+ rv = APR_SUCCESS;
+cleanup:
+ *pcert = (APR_SUCCESS == rv)? cert : NULL;
+ return rv;
+}
+
+apr_status_t md_cert_read_chain(apr_array_header_t *chain, apr_pool_t *p,
+ const char *pem, apr_size_t pem_len)
+{
+ BIO *bf = NULL;
+ apr_status_t rv = APR_SUCCESS;
+ md_cert_t *cert;
+ int added = 0;
+
+ if (NULL == (bf = BIO_new_mem_buf(pem, (int)pem_len))) {
+ rv = APR_ENOMEM;
+ goto cleanup;
+ }
+ while (APR_SUCCESS == (rv = md_cert_read_pem(bf, chain->pool, &cert))) {
+ APR_ARRAY_PUSH(chain, md_cert_t *) = cert;
+ added = 1;
+ }
+ if (APR_ENOENT == rv && added) {
+ rv = APR_SUCCESS;
+ }
+
+cleanup:
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "read chain with %d certs", chain->nelts);
+ if (bf) BIO_free(bf);
+ return rv;
+}
+
+apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *p,
+ const md_http_response_t *res)
+{
+ const char *ct;
+ apr_off_t data_len;
+ char *der;
+ apr_size_t der_len;
+ md_cert_t *cert = NULL;
+ apr_status_t rv;
+
+ ct = apr_table_get(res->headers, "Content-Type");
+ ct = md_util_parse_ct(res->req->pool, ct);
+ if (!res->body || !ct || strcmp("application/pkix-cert", ct)) {
+ rv = APR_ENOENT;
+ goto out;
+ }
+
+ if (APR_SUCCESS == (rv = apr_brigade_length(res->body, 1, &data_len))) {
+ if (data_len > 1024*1024) { /* certs usually are <2k each */
+ return APR_EINVAL;
+ }
+ if (APR_SUCCESS == (rv = apr_brigade_pflatten(res->body, &der, &der_len, res->req->pool))) {
+ const unsigned char *bf = (const unsigned char*)der;
+ X509 *x509;
+
+ if (NULL == (x509 = d2i_X509(NULL, &bf, (long)der_len))) {
+ rv = APR_EINVAL;
+ goto out;
+ }
+ else {
+ cert = md_cert_make(p, x509);
+ rv = APR_SUCCESS;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p,
+ "parsing cert from content-type=%s, content-length=%ld", ct, (long)data_len);
+ }
+ }
+ }
+out:
+ *pcert = (APR_SUCCESS == rv)? cert : NULL;
+ return rv;
+}
+
+apr_status_t md_cert_chain_read_http(struct apr_array_header_t *chain,
+ apr_pool_t *p, const struct md_http_response_t *res)
+{
+ const char *ct = NULL;
+ apr_off_t blen;
+ apr_size_t data_len = 0;
+ char *data;
+ md_cert_t *cert;
+ apr_status_t rv = APR_ENOENT;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p,
+ "chain_read, processing %d response", res->status);
+ if (APR_SUCCESS != (rv = apr_brigade_length(res->body, 1, &blen))) goto cleanup;
+ if (blen > 1024*1024) { /* certs usually are <2k each */
+ rv = APR_EINVAL;
+ goto cleanup;
+ }
+
+ data_len = (apr_size_t)blen;
+ ct = apr_table_get(res->headers, "Content-Type");
+ if (!res->body || !ct) goto cleanup;
+ ct = md_util_parse_ct(res->req->pool, ct);
+ if (!strcmp("application/pkix-cert", ct)) {
+ rv = md_cert_read_http(&cert, p, res);
+ if (APR_SUCCESS != rv) goto cleanup;
+ APR_ARRAY_PUSH(chain, md_cert_t *) = cert;
+ }
+ else if (!strcmp("application/pem-certificate-chain", ct)
+ || !strncmp("text/plain", ct, sizeof("text/plain")-1)) {
+ /* Some servers seem to think 'text/plain' is sufficient, see #232 */
+ rv = apr_brigade_pflatten(res->body, &data, &data_len, res->req->pool);
+ if (APR_SUCCESS != rv) goto cleanup;
+ rv = md_cert_read_chain(chain, res->req->pool, data, data_len);
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p,
+ "attempting to parse certificates from unrecognized content-type: %s", ct);
+ rv = apr_brigade_pflatten(res->body, &data, &data_len, res->req->pool);
+ if (APR_SUCCESS != rv) goto cleanup;
+ rv = md_cert_read_chain(chain, res->req->pool, data, data_len);
+ if (APR_SUCCESS == rv && chain->nelts == 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "certificate chain response did not contain any certificates "
+ "(suspicious content-type: %s)", ct);
+ rv = APR_ENOENT;
+ }
+ }
+cleanup:
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p,
+ "parsed certs from content-type=%s, content-length=%ld", ct, (long)data_len);
+ return rv;
+}
+
+md_cert_state_t md_cert_state_get(const md_cert_t *cert)
+{
+ if (cert->x509) {
+ return md_cert_is_valid_now(cert)? MD_CERT_VALID : MD_CERT_EXPIRED;
+ }
+ return MD_CERT_UNKNOWN;
+}
+
+apr_status_t md_chain_fappend(struct apr_array_header_t *certs, apr_pool_t *p, const char *fname)
+{
+ FILE *f;
+ apr_status_t rv;
+ X509 *x509;
+ md_cert_t *cert;
+ unsigned long err;
+
+ rv = md_util_fopen(&f, fname, "r");
+ if (rv == APR_SUCCESS) {
+ ERR_clear_error();
+ while (NULL != (x509 = PEM_read_X509(f, NULL, NULL, NULL))) {
+ cert = md_cert_make(p, x509);
+ APR_ARRAY_PUSH(certs, md_cert_t *) = cert;
+ }
+ fclose(f);
+
+ if (0 < (err = ERR_get_error())
+ && !(ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE)) {
+ /* not the expected one when no more PEM encodings are found */
+ rv = APR_EINVAL;
+ goto out;
+ }
+
+ if (certs->nelts == 0) {
+ /* Did not find any. This is acceptable unless the file has a certain size
+ * when we no longer accept it as empty chain file. Something seems to be
+ * wrong then. */
+ apr_finfo_t info;
+ if (APR_SUCCESS == apr_stat(&info, fname, APR_FINFO_SIZE, p) && info.size >= 1024) {
+ /* "Too big for a moon." */
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p,
+ "no certificates in non-empty chain %s", fname);
+ goto out;
+ }
+ }
+ }
+out:
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "read chain file %s, found %d certs",
+ fname, certs? certs->nelts : 0);
+ return rv;
+}
+
+apr_status_t md_chain_fload(apr_array_header_t **pcerts, apr_pool_t *p, const char *fname)
+{
+ apr_array_header_t *certs;
+ apr_status_t rv;
+
+ certs = apr_array_make(p, 5, sizeof(md_cert_t *));
+ rv = md_chain_fappend(certs, p, fname);
+ *pcerts = (APR_SUCCESS == rv)? certs : NULL;
+ return rv;
+}
+
+apr_status_t md_chain_fsave(apr_array_header_t *certs, apr_pool_t *p,
+ const char *fname, apr_fileperms_t perms)
+{
+ FILE *f;
+ apr_status_t rv;
+ const md_cert_t *cert;
+ unsigned long err = 0;
+ int i;
+
+ (void)p;
+ rv = md_util_fopen(&f, fname, "w");
+ if (rv == APR_SUCCESS) {
+ apr_file_perms_set(fname, perms);
+ ERR_clear_error();
+ for (i = 0; i < certs->nelts; ++i) {
+ cert = APR_ARRAY_IDX(certs, i, const md_cert_t *);
+ assert(cert->x509);
+
+ PEM_write_X509(f, cert->x509);
+
+ if (0 < (err = ERR_get_error())) {
+ break;
+ }
+
+ }
+ rv = fclose(f);
+ if (err) {
+ rv = APR_EINVAL;
+ }
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* certificate signing requests */
+
+static const char *alt_names(apr_array_header_t *domains, apr_pool_t *p)
+{
+ const char *alts = "", *sep = "", *domain;
+ int i;
+
+ for (i = 0; i < domains->nelts; ++i) {
+ domain = APR_ARRAY_IDX(domains, i, const char *);
+ alts = apr_psprintf(p, "%s%sDNS:%s", alts, sep, domain);
+ sep = ",";
+ }
+ return alts;
+}
+
+static apr_status_t add_ext(X509 *x, int nid, const char *value, apr_pool_t *p)
+{
+ X509_EXTENSION *ext = NULL;
+ X509V3_CTX ctx;
+ apr_status_t rv;
+
+ ERR_clear_error();
+ X509V3_set_ctx_nodb(&ctx);
+ X509V3_set_ctx(&ctx, x, x, NULL, NULL, 0);
+ if (NULL == (ext = X509V3_EXT_conf_nid(NULL, &ctx, nid, (char*)value))) {
+ unsigned long err = ERR_get_error();
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext, create, nid=%d value='%s' "
+ "(lib=%d, reason=%d)", nid, value, ERR_GET_LIB(err), ERR_GET_REASON(err));
+ return APR_EGENERAL;
+ }
+
+ ERR_clear_error();
+ rv = X509_add_ext(x, ext, -1)? APR_SUCCESS : APR_EINVAL;
+ if (APR_SUCCESS != rv) {
+ unsigned long err = ERR_get_error();
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext, add, nid=%d value='%s' "
+ "(lib=%d, reason=%d)", nid, value, ERR_GET_LIB(err), ERR_GET_REASON(err));
+ }
+ X509_EXTENSION_free(ext);
+ return rv;
+}
+
+static apr_status_t sk_add_alt_names(STACK_OF(X509_EXTENSION) *exts,
+ apr_array_header_t *domains, apr_pool_t *p)
+{
+ if (domains->nelts > 0) {
+ X509_EXTENSION *x;
+
+ x = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, (char*)alt_names(domains, p));
+ if (NULL == x) {
+ return APR_EGENERAL;
+ }
+ sk_X509_EXTENSION_push(exts, x);
+ }
+ return APR_SUCCESS;
+}
+
+#define MD_OID_MUST_STAPLE_NUM "1.3.6.1.5.5.7.1.24"
+#define MD_OID_MUST_STAPLE_SNAME "tlsfeature"
+#define MD_OID_MUST_STAPLE_LNAME "TLS Feature"
+
+int md_cert_must_staple(const md_cert_t *cert)
+{
+ /* In case we do not get the NID for it, we treat this as not set. */
+ int nid = MD_GET_NID(MUST_STAPLE);
+ return ((NID_undef != nid)) && X509_get_ext_by_NID(cert->x509, nid, -1) >= 0;
+}
+
+static apr_status_t add_must_staple(STACK_OF(X509_EXTENSION) *exts, const char *name, apr_pool_t *p)
+{
+ X509_EXTENSION *x;
+ int nid;
+
+ nid = MD_GET_NID(MUST_STAPLE);
+ if (NID_undef == nid) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "%s: unable to get NID for v3 must-staple TLS feature", name);
+ return APR_ENOTIMPL;
+ }
+ x = X509V3_EXT_conf_nid(NULL, NULL, nid, (char*)"DER:30:03:02:01:05");
+ if (NULL == x) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "%s: unable to create x509 extension for must-staple", name);
+ return APR_EGENERAL;
+ }
+ sk_X509_EXTENSION_push(exts, x);
+ return APR_SUCCESS;
+}
+
+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_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);
+ md_data_null(&csr_der);
+
+ if (NULL == (csr = X509_REQ_new())
+ || NULL == (exts = sk_X509_EXTENSION_new_null())
+ || NULL == (n = X509_NAME_new())) {
+ rv = APR_ENOMEM;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: openssl alloc X509 things", name);
+ goto out;
+ }
+
+ /* subject name == first domain */
+ domain = APR_ARRAY_IDX(domains, 0, const unsigned char *);
+ /* Do not set the domain in the CN if it is longer than 64 octets.
+ * Instead, let the CA choose a 'proper' name. At the moment (2021-01), LE will
+ * inspect all SAN names and use one < 64 chars if it can be found. It will fail
+ * otherwise.
+ * The reason we do not check this beforehand is that the restrictions on CNs
+ * are in flux. They used to be authoritative, now browsers no longer do that, but
+ * no one wants to hand out a cert with "google.com" as CN either. So, we leave
+ * it for the CA to decide if and how it hands out a cert for this or fails.
+ * This solves issue where the name is too long, see #227 */
+ if (strlen((const char*)domain) < 64
+ && (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, domain, -1, -1, 0)
+ || !X509_REQ_set_subject_name(csr, n))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: REQ name add entry", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* collect extensions, such as alt names and must staple */
+ if (APR_SUCCESS != (rv = sk_add_alt_names(exts, domains, p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: collecting alt names", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ if (must_staple && APR_SUCCESS != (rv = add_must_staple(exts, name, p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: you requested that a certificate "
+ "is created with the 'must-staple' extension, however the SSL library was "
+ "unable to initialized that extension. Please file a bug report on which platform "
+ "and with which library this happens. To continue before this problem is resolved, "
+ "configure 'MDMustStaple off' for your domains", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* add extensions to csr */
+ if (sk_X509_EXTENSION_num(exts) > 0 && !X509_REQ_add_extensions(csr, exts)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: adding exts", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* add our key */
+ if (!X509_REQ_set_pubkey(csr, pkey->pkey)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in csr", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* sign, der encode and base64url encode */
+ if (!X509_REQ_sign(csr, pkey->pkey, pkey_get_MD(pkey))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign csr", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ if ((csr_der_len = i2d_X509_REQ(csr, NULL)) < 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: der length", name);
+ rv = APR_EGENERAL; goto out;
+ }
+ 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, p);
+ rv = APR_SUCCESS;
+
+out:
+ if (exts) {
+ sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
+ }
+ if (csr) {
+ X509_REQ_free(csr);
+ }
+ if (n) {
+ X509_NAME_free(n);
+ }
+ *pcsr_der_64 = (APR_SUCCESS == rv)? csr_der_64 : NULL;
+ return rv;
+}
+
+static apr_status_t mk_x509(X509 **px, md_pkey_t *pkey, const char *cn,
+ apr_interval_time_t valid_for, apr_pool_t *p)
+{
+ X509 *x = NULL;
+ X509_NAME *n = NULL;
+ BIGNUM *big_rnd = NULL;
+ ASN1_INTEGER *asn1_rnd = NULL;
+ unsigned char rnd[20];
+ int days;
+ apr_status_t rv;
+
+ if (NULL == (x = X509_new())
+ || NULL == (n = X509_NAME_new())) {
+ rv = APR_ENOMEM;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: openssl alloc X509 things", cn);
+ goto out;
+ }
+
+ if (APR_SUCCESS != (rv = md_rand_bytes(rnd, sizeof(rnd), p))
+ || !(big_rnd = BN_bin2bn(rnd, sizeof(rnd), NULL))
+ || !(asn1_rnd = BN_to_ASN1_INTEGER(big_rnd, NULL))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setup random serial", cn);
+ rv = APR_EGENERAL; goto out;
+ }
+ if (!X509_set_serialNumber(x, asn1_rnd)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: set serial number", cn);
+ rv = APR_EGENERAL; goto out;
+ }
+ if (1 != X509_set_version(x, 2L)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setting x.509v3", cn);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* set common name and issuer */
+ if (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, (const unsigned char*)cn, -1, -1, 0)
+ || !X509_set_subject_name(x, n)
+ || !X509_set_issuer_name(x, n)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: name add entry", cn);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* cert are unconstrained (but not very trustworthy) */
+ if (APR_SUCCESS != (rv = add_ext(x, NID_basic_constraints, "critical,CA:FALSE", p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set basic constraints ext", cn);
+ goto out;
+ }
+ /* add our key */
+ if (!X509_set_pubkey(x, pkey->pkey)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in x509", cn);
+ rv = APR_EGENERAL; goto out;
+ }
+ /* validity */
+ days = (int)((apr_time_sec(valid_for) + MD_SECS_PER_DAY - 1)/ MD_SECS_PER_DAY);
+ if (!X509_set_notBefore(x, ASN1_TIME_set(NULL, time(NULL)))) {
+ rv = APR_EGENERAL; goto out;
+ }
+ if (!X509_set_notAfter(x, ASN1_TIME_adj(NULL, time(NULL), days, 0))) {
+ rv = APR_EGENERAL; goto out;
+ }
+
+out:
+ *px = (APR_SUCCESS == rv)? x : NULL;
+ if (APR_SUCCESS != rv && x) X509_free(x);
+ if (big_rnd) BN_free(big_rnd);
+ if (asn1_rnd) ASN1_INTEGER_free(asn1_rnd);
+ if (n) X509_NAME_free(n);
+ return rv;
+}
+
+apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn,
+ apr_array_header_t *domains, md_pkey_t *pkey,
+ apr_interval_time_t valid_for, apr_pool_t *p)
+{
+ X509 *x;
+ md_cert_t *cert = NULL;
+ apr_status_t rv;
+
+ assert(domains);
+
+ if (APR_SUCCESS != (rv = mk_x509(&x, pkey, cn, valid_for, p))) goto out;
+
+ /* add the domain as alt name */
+ if (APR_SUCCESS != (rv = add_ext(x, NID_subject_alt_name, alt_names(domains, p), p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", cn);
+ goto out;
+ }
+
+ /* keyUsage, ExtendedKeyUsage */
+
+ if (APR_SUCCESS != (rv = add_ext(x, NID_key_usage, "critical,digitalSignature", p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set keyUsage", cn);
+ goto out;
+ }
+ if (APR_SUCCESS != (rv = add_ext(x, NID_ext_key_usage, "serverAuth", p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set extKeyUsage", cn);
+ goto out;
+ }
+
+ /* sign with same key */
+ if (!X509_sign(x, pkey->pkey, pkey_get_MD(pkey))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", cn);
+ rv = APR_EGENERAL; goto out;
+ }
+
+ cert = md_cert_make(p, x);
+ rv = APR_SUCCESS;
+
+out:
+ *pcert = (APR_SUCCESS == rv)? cert : NULL;
+ if (!cert && x) X509_free(x);
+ return rv;
+}
+
+#define MD_OID_ACME_VALIDATION_NUM "1.3.6.1.5.5.7.1.31"
+#define MD_OID_ACME_VALIDATION_SNAME "pe-acmeIdentifier"
+#define MD_OID_ACME_VALIDATION_LNAME "ACME Identifier"
+
+static int get_acme_validation_nid(void)
+{
+ int nid = OBJ_txt2nid(MD_OID_ACME_VALIDATION_NUM);
+ if (NID_undef == nid) {
+ nid = OBJ_create(MD_OID_ACME_VALIDATION_NUM,
+ MD_OID_ACME_VALIDATION_SNAME, MD_OID_ACME_VALIDATION_LNAME);
+ }
+ return nid;
+}
+
+apr_status_t md_cert_make_tls_alpn_01(md_cert_t **pcert, const char *domain,
+ const char *acme_id, md_pkey_t *pkey,
+ apr_interval_time_t valid_for, apr_pool_t *p)
+{
+ X509 *x;
+ md_cert_t *cert = NULL;
+ const char *alts;
+ apr_status_t rv;
+
+ if (APR_SUCCESS != (rv = mk_x509(&x, pkey, "tls-alpn-01-challenge", valid_for, p))) goto out;
+
+ /* add the domain as alt name */
+ alts = apr_psprintf(p, "DNS:%s", domain);
+ if (APR_SUCCESS != (rv = add_ext(x, NID_subject_alt_name, alts, p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", domain);
+ goto out;
+ }
+
+ if (APR_SUCCESS != (rv = add_ext(x, get_acme_validation_nid(), acme_id, p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pe-acmeIdentifier", domain);
+ goto out;
+ }
+
+ /* sign with same key */
+ if (!X509_sign(x, pkey->pkey, pkey_get_MD(pkey))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", domain);
+ rv = APR_EGENERAL; goto out;
+ }
+
+ cert = md_cert_make(p, x);
+ rv = APR_SUCCESS;
+
+out:
+ if (!cert && x) {
+ X509_free(x);
+ }
+ *pcert = (APR_SUCCESS == rv)? cert : NULL;
+ return rv;
+}
+
+#define MD_OID_CT_SCTS_NUM "1.3.6.1.4.1.11129.2.4.2"
+#define MD_OID_CT_SCTS_SNAME "CT-SCTs"
+#define MD_OID_CT_SCTS_LNAME "CT Certificate SCTs"
+
+#ifndef OPENSSL_NO_CT
+static int get_ct_scts_nid(void)
+{
+ int nid = OBJ_txt2nid(MD_OID_CT_SCTS_NUM);
+ if (NID_undef == nid) {
+ nid = OBJ_create(MD_OID_CT_SCTS_NUM,
+ MD_OID_CT_SCTS_SNAME, MD_OID_CT_SCTS_LNAME);
+ }
+ return nid;
+}
+#endif
+
+const char *md_nid_get_sname(int nid)
+{
+ return OBJ_nid2sn(nid);
+}
+
+const char *md_nid_get_lname(int nid)
+{
+ return OBJ_nid2ln(nid);
+}
+
+apr_status_t md_cert_get_ct_scts(apr_array_header_t *scts, apr_pool_t *p, const md_cert_t *cert)
+{
+#ifndef OPENSSL_NO_CT
+ int nid, i, idx, critical;
+ STACK_OF(SCT) *sct_list;
+ SCT *sct_handle;
+ md_sct *sct;
+ size_t len;
+ const char *data;
+
+ nid = get_ct_scts_nid();
+ if (NID_undef == nid) return APR_ENOTIMPL;
+
+ idx = -1;
+ while (1) {
+ sct_list = X509_get_ext_d2i(cert->x509, nid, &critical, &idx);
+ if (sct_list) {
+ for (i = 0; i < sk_SCT_num(sct_list); i++) {
+ sct_handle = sk_SCT_value(sct_list, i);
+ if (sct_handle) {
+ sct = apr_pcalloc(p, sizeof(*sct));
+ sct->version = SCT_get_version(sct_handle);
+ sct->timestamp = apr_time_from_msec(SCT_get_timestamp(sct_handle));
+ len = SCT_get0_log_id(sct_handle, (unsigned char**)&data);
+ sct->logid = md_data_make_pcopy(p, data, len);
+ sct->signature_type_nid = SCT_get_signature_nid(sct_handle);
+ len = SCT_get0_signature(sct_handle, (unsigned char**)&data);
+ sct->signature = md_data_make_pcopy(p, data, len);
+
+ APR_ARRAY_PUSH(scts, md_sct*) = sct;
+ }
+ }
+ }
+ if (idx < 0) break;
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "ct_sct, found %d SCT extensions", scts->nelts);
+ return APR_SUCCESS;
+#else
+ (void)scts;
+ (void)p;
+ (void)cert;
+ return APR_ENOTIMPL;
+#endif
+}
+
+apr_status_t md_cert_get_ocsp_responder_url(const char **purl, apr_pool_t *p, const md_cert_t *cert)
+{
+ STACK_OF(OPENSSL_STRING) *ssk;
+ apr_status_t rv = APR_SUCCESS;
+ const char *url = NULL;
+
+ ssk = X509_get1_ocsp(md_cert_get_X509(cert));
+ if (!ssk) {
+ rv = APR_ENOENT;
+ goto cleanup;
+ }
+ url = apr_pstrdup(p, sk_OPENSSL_STRING_value(ssk, 0));
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, "ocsp responder found '%s'", url);
+
+cleanup:
+ if (ssk) X509_email_free(ssk);
+ *purl = url;
+ return rv;
+}
+
+apr_status_t md_check_cert_and_pkey(struct apr_array_header_t *certs, md_pkey_t *pkey)
+{
+ const md_cert_t *cert;
+
+ if (certs->nelts == 0) {
+ return APR_ENOENT;
+ }
+
+ cert = APR_ARRAY_IDX(certs, 0, const md_cert_t*);
+
+ if (1 != X509_check_private_key(cert->x509, pkey->pkey)) {
+ return APR_EGENERAL;
+ }
+
+ return APR_SUCCESS;
+}
diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h
new file mode 100644
index 0000000..a892e00
--- /dev/null
+++ b/modules/md/md_crypt.h
@@ -0,0 +1,253 @@
+/* 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_crypt_h
+#define mod_md_md_crypt_h
+
+#include <apr_file_io.h>
+
+struct apr_array_header_t;
+struct md_t;
+struct md_http_response_t;
+struct md_cert_t;
+struct md_pkey_t;
+struct md_data_t;
+struct md_timeperiod_t;
+
+/**************************************************************************************************/
+/* random */
+
+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_t *data);
+apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p,
+ const struct md_data_t *data);
+
+/**************************************************************************************************/
+/* private keys */
+
+typedef struct md_pkey_t md_pkey_t;
+
+typedef enum {
+ MD_PKEY_TYPE_DEFAULT,
+ MD_PKEY_TYPE_RSA,
+ MD_PKEY_TYPE_EC,
+} md_pkey_type_t;
+
+typedef struct md_pkey_rsa_params_t {
+ apr_uint32_t bits;
+} md_pkey_rsa_params_t;
+
+typedef struct md_pkey_ec_params_t {
+ const char *curve;
+} md_pkey_ec_params_t;
+
+typedef struct md_pkey_spec_t {
+ md_pkey_type_t type;
+ union {
+ md_pkey_rsa_params_t rsa;
+ md_pkey_ec_params_t ec;
+ } params;
+} md_pkey_spec_t;
+
+typedef struct md_pkeys_spec_t {
+ apr_pool_t *p;
+ struct apr_array_header_t *specs;
+} md_pkeys_spec_t;
+
+apr_status_t md_crypt_init(apr_pool_t *pool);
+
+const char *md_pkey_spec_name(const md_pkey_spec_t *spec);
+
+md_pkeys_spec_t *md_pkeys_spec_make(apr_pool_t *p);
+void md_pkeys_spec_add_default(md_pkeys_spec_t *pks);
+int md_pkeys_spec_contains_rsa(md_pkeys_spec_t *pks);
+void md_pkeys_spec_add_rsa(md_pkeys_spec_t *pks, unsigned int bits);
+int md_pkeys_spec_contains_ec(md_pkeys_spec_t *pks, const char *curve);
+void md_pkeys_spec_add_ec(md_pkeys_spec_t *pks, const char *curve);
+int md_pkeys_spec_eq(md_pkeys_spec_t *pks1, md_pkeys_spec_t *pks2);
+md_pkeys_spec_t *md_pkeys_spec_clone(apr_pool_t *p, const md_pkeys_spec_t *pks);
+int md_pkeys_spec_is_empty(const md_pkeys_spec_t *pks);
+md_pkey_spec_t *md_pkeys_spec_get(const md_pkeys_spec_t *pks, int index);
+int md_pkeys_spec_count(const md_pkeys_spec_t *pks);
+void md_pkeys_spec_add(md_pkeys_spec_t *pks, md_pkey_spec_t *spec);
+
+struct md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p);
+md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p);
+struct md_json_t *md_pkeys_spec_to_json(const md_pkeys_spec_t *pks, apr_pool_t *p);
+md_pkeys_spec_t *md_pkeys_spec_from_json(struct md_json_t *json, apr_pool_t *p);
+
+
+apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *key_props);
+void md_pkey_free(md_pkey_t *pkey);
+
+const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p);
+const char *md_pkey_get_rsa_n64(md_pkey_t *pkey, apr_pool_t *p);
+
+apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p,
+ const char *pass_phrase, apr_size_t pass_len,
+ const char *fname);
+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);
+
+apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *p,
+ const char *d, size_t dlen);
+
+void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey);
+
+apr_status_t md_crypt_hmac64(const char **pmac64, const struct md_data_t *hmac_key,
+ apr_pool_t *p, const char *d, size_t dlen);
+
+/**
+ * Read a private key from a http response.
+ */
+apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool,
+ const struct md_http_response_t *res);
+
+/**************************************************************************************************/
+/* X509 certificates */
+
+typedef struct md_cert_t md_cert_t;
+
+typedef enum {
+ MD_CERT_UNKNOWN,
+ MD_CERT_VALID,
+ MD_CERT_EXPIRED
+} md_cert_state_t;
+
+/**
+ * Create a holder of the certificate that will free its memory 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);
+apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p,
+ const char *fname, apr_fileperms_t perms);
+
+/**
+ * Read a x509 certificate from a http response.
+ * Will return APR_ENOENT if content-type is not recognized (currently
+ * only "application/pkix-cert" is supported).
+ */
+apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *pool,
+ const struct md_http_response_t *res);
+
+/**
+ * Read at least one certificate from the given PEM data.
+ */
+apr_status_t md_cert_read_chain(apr_array_header_t *chain, apr_pool_t *p,
+ const char *pem, apr_size_t pem_len);
+
+/**
+ * Read one or even a chain of certificates from a http response.
+ * Will return APR_ENOENT if content-type is not recognized (currently
+ * supports only "application/pem-certificate-chain" and "application/pkix-cert").
+ * @param chain must be non-NULL, retrieved certificates will be added.
+ */
+apr_status_t md_cert_chain_read_http(struct apr_array_header_t *chain,
+ apr_pool_t *pool, const struct md_http_response_t *res);
+
+md_cert_state_t md_cert_state_get(const md_cert_t *cert);
+int md_cert_is_valid_now(const md_cert_t *cert);
+int md_cert_has_expired(const md_cert_t *cert);
+int md_cert_covers_domain(md_cert_t *cert, const char *domain_name);
+int md_cert_covers_md(md_cert_t *cert, const struct md_t *md);
+int md_cert_must_staple(const md_cert_t *cert);
+apr_time_t md_cert_get_not_after(const md_cert_t *cert);
+apr_time_t md_cert_get_not_before(const md_cert_t *cert);
+struct md_timeperiod_t md_cert_get_valid(const md_cert_t *cert);
+
+/**
+ * Return != 0 iff the hash values of the certificates are equal.
+ */
+int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b);
+
+apr_status_t md_cert_get_issuers_uri(const char **puri, const md_cert_t *cert, apr_pool_t *p);
+apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, const 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);
+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_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);
+
+apr_status_t md_chain_fload(struct apr_array_header_t **pcerts,
+ apr_pool_t *p, const char *fname);
+apr_status_t md_chain_fsave(struct apr_array_header_t *certs,
+ apr_pool_t *p, const char *fname, apr_fileperms_t perms);
+apr_status_t md_chain_fappend(struct apr_array_header_t *certs,
+ apr_pool_t *p, const char *fname);
+
+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);
+
+/**
+ * Create a self-signed cerftificate with the given cn, key and list
+ * of alternate domain names.
+ */
+apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn,
+ struct apr_array_header_t *domains, md_pkey_t *pkey,
+ apr_interval_time_t valid_for, apr_pool_t *p);
+
+/**
+ * Create a certificate for answering "tls-alpn-01" ACME challenges
+ * (see <https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01>).
+ */
+apr_status_t md_cert_make_tls_alpn_01(md_cert_t **pcert, const char *domain,
+ const char *acme_id, md_pkey_t *pkey,
+ apr_interval_time_t valid_for, apr_pool_t *p);
+
+apr_status_t md_cert_get_ct_scts(apr_array_header_t *scts, apr_pool_t *p, const md_cert_t *cert);
+
+apr_status_t md_cert_get_ocsp_responder_url(const char **purl, apr_pool_t *p, const md_cert_t *cert);
+
+apr_status_t md_check_cert_and_pkey(struct apr_array_header_t *certs, md_pkey_t *pkey);
+
+
+/**************************************************************************************************/
+/* X509 certificate transparency */
+
+const char *md_nid_get_sname(int nid);
+const char *md_nid_get_lname(int nid);
+
+typedef struct md_sct md_sct;
+struct md_sct {
+ int version;
+ apr_time_t timestamp;
+ struct md_data_t *logid;
+ int signature_type_nid;
+ struct md_data_t *signature;
+};
+
+#endif /* md_crypt_h */
diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c
new file mode 100644
index 0000000..217e857
--- /dev/null
+++ b/modules/md/md_curl.c
@@ -0,0 +1,653 @@
+/* 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 <curl/curl.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#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(unsigned int curl_code)
+{
+ switch (curl_code) {
+ case CURLE_OK: return APR_SUCCESS;
+ case CURLE_UNSUPPORTED_PROTOCOL: return APR_ENOTIMPL;
+ case CURLE_NOT_BUILT_IN: return APR_ENOTIMPL;
+ case CURLE_URL_MALFORMAT: return APR_EINVAL;
+ case CURLE_COULDNT_RESOLVE_PROXY:return APR_ECONNREFUSED;
+ case CURLE_COULDNT_RESOLVE_HOST: return APR_ECONNREFUSED;
+ case CURLE_COULDNT_CONNECT: return APR_ECONNREFUSED;
+ case CURLE_REMOTE_ACCESS_DENIED: return APR_EACCES;
+ case CURLE_OUT_OF_MEMORY: return APR_ENOMEM;
+ case CURLE_OPERATION_TIMEDOUT: return APR_TIMEUP;
+ case CURLE_SSL_CONNECT_ERROR: return APR_ECONNABORTED;
+ case CURLE_AGAIN: return APR_EAGAIN;
+ default: return APR_EGENERAL;
+ }
+}
+
+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;
+ size_t blen, read_len = 0, max_len = len * nmemb;
+ const char *bdata;
+ char *rdata = data;
+ apr_bucket *b;
+ apr_status_t rv;
+
+ while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) {
+ b = APR_BRIGADE_FIRST(body);
+ if (APR_BUCKET_IS_METADATA(b)) {
+ if (APR_BUCKET_IS_EOS(b)) {
+ body = NULL;
+ }
+ }
+ else {
+ rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ);
+ if (rv == APR_SUCCESS) {
+ if (blen > max_len) {
+ apr_bucket_split(b, max_len);
+ blen = max_len;
+ }
+ memcpy(rdata, bdata, blen);
+ read_len += blen;
+ max_len -= blen;
+ rdata += blen;
+ }
+ else {
+ body = NULL;
+ if (!APR_STATUS_IS_EOF(rv)) {
+ /* everything beside EOF is an error */
+ read_len = CURL_READFUNC_ABORT;
+ }
+ }
+
+ }
+ apr_bucket_delete(b);
+ }
+
+ return read_len;
+}
+
+static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton)
+{
+ md_curl_internals_t *internals = baton;
+ md_http_response_t *res = internals->response;
+ size_t blen = len * nmemb;
+ apr_status_t rv;
+
+ if (res->body) {
+ 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)blen > res->req->resp_limit) {
+ return 0; /* signal curl failure */
+ }
+ }
+ rv = apr_brigade_write(res->body, NULL, NULL, (const char *)data, blen);
+ if (rv != APR_SUCCESS) {
+ /* returning anything != blen will make CURL fail this */
+ return 0;
+ }
+ }
+ return blen;
+}
+
+static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *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;
+
+ len = (clen && b[clen-1] == '\n')? clen-1 : clen;
+ len = (len && b[len-1] == '\r')? len-1 : len;
+ for (i = 0; i < len; ++i) {
+ if (b[i] == ':') {
+ name = apr_pstrndup(res->req->pool, b, i);
+ ++i;
+ while (i < len && b[i] == ' ') {
+ ++i;
+ }
+ if (i < len) {
+ value = apr_pstrndup(res->req->pool, b+i, len - i);
+ }
+ break;
+ }
+ }
+
+ if (name != NULL) {
+ apr_table_add(res->headers, name, value);
+ }
+ return clen;
+}
+
+typedef struct {
+ md_http_request_t *req;
+ struct curl_slist *hdrs;
+ apr_status_t rv;
+} curlify_hdrs_ctx;
+
+static int curlify_headers(void *baton, const char *key, const char *value)
+{
+ curlify_hdrs_ctx *ctx = baton;
+ const char *s;
+
+ if (strchr(key, '\r') || strchr(key, '\n')
+ || strchr(value, '\r') || strchr(value, '\n')) {
+ ctx->rv = APR_EINVAL;
+ return 0;
+ }
+ s = apr_psprintf(ctx->req->pool, "%s: %s", key, value);
+ ctx->hdrs = curl_slist_append(ctx->hdrs, s);
+ return 1;
+}
+
+/* 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)
+{
+ long ms = (long)apr_time_as_msec(timeout);
+ return ms? ms : (timeout? 1 : 0);
+}
+
+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;
+
+ (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;
+ md_data_init(&d, data, 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;
+ md_data_init(&d, data, 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;
+
+ curl = md_http_get_impl_data(req->http);
+ if (!curl) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "creating curl instance");
+ curl = curl_easy_init();
+ if (!curl) {
+ 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");
+ }
+
+ internals = apr_pcalloc(req->pool, sizeof(*internals));
+ internals->curl = curl;
+
+ 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)) {
+ /* nop */
+ }
+ else if (!apr_strnatcasecmp("HEAD", req->method)) {
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
+ }
+ else if (!apr_strnatcasecmp("POST", req->method)) {
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
+ }
+ else {
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method);
+ }
+ curl_easy_setopt(curl, CURLOPT_HEADERDATA, internals);
+ curl_easy_setopt(curl, CURLOPT_READDATA, req->body);
+ 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->ca_file) {
+ curl_easy_setopt(curl, CURLOPT_CAINFO, req->ca_file);
+ }
+ if (req->unix_socket_path) {
+ curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, req->unix_socket_path);
+ }
+
+ if (req->body_len >= 0) {
+ /* set the Content-Length */
+ curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)req->body_len);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)req->body_len);
+ }
+
+ if (req->user_agent) {
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent);
+ }
+ if (req->proxy_url) {
+ curl_easy_setopt(curl, CURLOPT_PROXY, req->proxy_url);
+ }
+ if (!apr_is_empty_table(req->headers)) {
+ curlify_hdrs_ctx ctx;
+
+ ctx.req = req;
+ ctx.hdrs = NULL;
+ ctx.rv = APR_SUCCESS;
+ apr_table_do(curlify_headers, &ctx, req->headers, NULL);
+ internals->req_hdrs = ctx.hdrs;
+ if (ctx.rv == APR_SUCCESS) {
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, internals->req_hdrs);
+ }
+ }
+
+ 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_TRACE4)) {
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+ curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_log);
+ curl_easy_setopt(curl, CURLOPT_DEBUGDATA, req);
+ }
+
+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;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, req->pool,
+ "req[%d]: http status is %d",
+ req->id, internals->response->status);
+ }
+ }
+ 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);
+ }
+
+ internals->rv = rv;
+ if (req->cb.on_status) {
+ req->cb.on_status(req, rv, req->cb.on_status_data);
+ }
+ }
+}
+
+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;
+ }
+
+ 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);
+
+ 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);
+ 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;
+
+ 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_and_destroy(md_http_request_t *req, CURLM *curlm)
+{
+ md_curl_internals_t *internals = req->internals;
+
+ 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) {
+ rv = APR_ENOMEM;
+ goto leave;
+ }
+
+ running = 1;
+ slowdown = 0;
+ while(1) {
+ while (1) {
+ /* fetch as many requests as nextreq gives us */
+ 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;
+ }
+ }
+
+ 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 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);
+ 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 (running < requests->nelts) {
+ 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_TRACE2, 0, p,
+ "multi_perform[%d reqs]: req[%d] done",
+ requests->nelts, req->id);
+ update_status(req);
+ fire_status(req, curl_status(curlmsg->data.result));
+ md_array_remove(requests, 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,
+ "multi_perform[%d reqs]: req done, but not found by handle",
+ 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);
+ 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;
+}
+
+static int initialized;
+
+static apr_status_t md_curl_init(void) {
+ if (!initialized) {
+ initialized = 1;
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+ }
+ return APR_SUCCESS;
+}
+
+static void md_curl_req_cleanup(md_http_request_t *req)
+{
+ md_curl_internals_t *internals = req->internals;
+ if (internals) {
+ if (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;
+ }
+}
+
+static void md_curl_cleanup(md_http_t *http, apr_pool_t *pool)
+{
+ CURL *curl;
+
+ curl = md_http_get_impl_data(http);
+ if (curl) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, pool, "cleanup curl instance");
+ md_http_set_impl_data(http, NULL);
+ curl_easy_cleanup(curl);
+ }
+}
+
+static md_http_impl_t impl = {
+ md_curl_init,
+ md_curl_req_cleanup,
+ md_curl_perform,
+ md_curl_multi_perform,
+ md_curl_cleanup,
+};
+
+md_http_impl_t * md_curl_get_impl(apr_pool_t *p)
+{
+ /* trigger early global curl init, before we are down a rabbit hole */
+ (void)p;
+ md_curl_init();
+ return &impl;
+}
diff --git a/modules/md/md_curl.h b/modules/md/md_curl.h
new file mode 100644
index 0000000..cbc1dd2
--- /dev/null
+++ b/modules/md/md_curl.h
@@ -0,0 +1,24 @@
+/* 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_curl_h
+#define md_curl_h
+
+struct md_http_impl;
+
+struct md_http_impl_t * md_curl_get_impl(apr_pool_t *p);
+
+#endif /* md_curl_h */
diff --git a/modules/md/md_event.c b/modules/md/md_event.c
new file mode 100644
index 0000000..c731d55
--- /dev/null
+++ b/modules/md/md_event.c
@@ -0,0 +1,89 @@
+/* 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_strings.h>
+
+#include "md.h"
+#include "md_event.h"
+
+
+typedef struct md_subscription {
+ struct md_subscription *next;
+ md_event_cb *cb;
+ void *baton;
+} md_subscription;
+
+static struct {
+ apr_pool_t *p;
+ md_subscription *subs;
+} EVNT;
+
+static apr_status_t cleanup_setup(void *dummy)
+{
+ (void)dummy;
+ memset(&EVNT, 0, sizeof(EVNT));
+ return APR_SUCCESS;
+}
+
+void md_event_init(apr_pool_t *p)
+{
+ memset(&EVNT, 0, sizeof(EVNT));
+ EVNT.p = p;
+ apr_pool_cleanup_register(p, NULL, cleanup_setup, apr_pool_cleanup_null);
+}
+
+void md_event_subscribe(md_event_cb *cb, void *baton)
+{
+ md_subscription *sub;
+
+ sub = apr_pcalloc(EVNT.p, sizeof(*sub));
+ sub->cb = cb;
+ sub->baton = baton;
+ sub->next = EVNT.subs;
+ EVNT.subs = sub;
+}
+
+apr_status_t md_event_raise(const char *event,
+ const char *mdomain,
+ struct md_job_t *job,
+ struct md_result_t *result,
+ apr_pool_t *p)
+{
+ md_subscription *sub = EVNT.subs;
+ apr_status_t rv;
+
+ while (sub) {
+ rv = sub->cb(event, mdomain, sub->baton, job, result, p);
+ if (APR_SUCCESS != rv) return rv;
+ sub = sub->next;
+ }
+ return APR_SUCCESS;
+}
+
+void md_event_holler(const char *event,
+ const char *mdomain,
+ struct md_job_t *job,
+ struct md_result_t *result,
+ apr_pool_t *p)
+{
+ md_subscription *sub = EVNT.subs;
+ while (sub) {
+ sub->cb(event, mdomain, sub->baton, job, result, p);
+ sub = sub->next;
+ }
+}
diff --git a/modules/md/md_event.h b/modules/md/md_event.h
new file mode 100644
index 0000000..e66c3c2
--- /dev/null
+++ b/modules/md/md_event.h
@@ -0,0 +1,46 @@
+/* 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_event_h
+#define md_event_h
+
+struct md_job_t;
+struct md_result_t;
+
+typedef apr_status_t md_event_cb(const char *event,
+ const char *mdomain,
+ void *baton,
+ struct md_job_t *job,
+ struct md_result_t *result,
+ apr_pool_t *p);
+
+void md_event_init(apr_pool_t *p);
+
+void md_event_subscribe(md_event_cb *cb, void *baton);
+
+apr_status_t md_event_raise(const char *event,
+ const char *mdomain,
+ struct md_job_t *job,
+ struct md_result_t *result,
+ apr_pool_t *p);
+
+void md_event_holler(const char *event,
+ const char *mdomain,
+ struct md_job_t *job,
+ struct md_result_t *result,
+ apr_pool_t *p);
+
+#endif /* md_event_h */
diff --git a/modules/md/md_http.c b/modules/md/md_http.c
new file mode 100644
index 0000000..0d21e7b
--- /dev/null
+++ b/modules/md/md_http.c
@@ -0,0 +1,397 @@
+/* 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_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#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;
+ void *impl_data; /* to be used by the implementation */
+ const char *user_agent;
+ const char *proxy_url;
+ const char *unix_socket_path;
+ md_http_timeouts_t timeout;
+ const char *ca_file;
+};
+
+static md_http_impl_t *cur_impl;
+static int cur_init_done;
+
+void md_http_use_implementation(md_http_impl_t *impl)
+{
+ if (cur_impl != impl) {
+ cur_impl = impl;
+ cur_init_done = 0;
+ }
+}
+
+static apr_status_t http_cleanup(void *data)
+{
+ md_http_t *http = data;
+ if (http && http->impl && http->impl->cleanup) {
+ http->impl->cleanup(http, http->pool);
+ }
+ return APR_SUCCESS;
+}
+
+apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent,
+ const char *proxy_url)
+{
+ md_http_t *http;
+ apr_status_t rv = APR_SUCCESS;
+
+ if (!cur_impl) {
+ *phttp = NULL;
+ return APR_ENOTIMPL;
+ }
+
+ if (!cur_init_done) {
+ if (APR_SUCCESS == (rv = cur_impl->init())) {
+ cur_init_done = 1;
+ }
+ else {
+ return rv;
+ }
+ }
+
+ http = apr_pcalloc(p, sizeof(*http));
+ http->pool = p;
+ http->impl = cur_impl;
+ http->user_agent = apr_pstrdup(p, user_agent);
+ http->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
+ http->bucket_alloc = apr_bucket_alloc_create(p);
+ if (!http->bucket_alloc) {
+ return APR_EGENERAL;
+ }
+ apr_pool_cleanup_register(p, http, http_cleanup, apr_pool_cleanup_null);
+ *phttp = http;
+ 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;
+}
+
+void *md_http_get_impl_data(md_http_t *http)
+{
+ return http->impl_data;
+}
+
+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;
+}
+
+void md_http_set_ca_file(md_http_t *http, const char *ca_file)
+{
+ http->ca_file = ca_file;
+}
+
+void md_http_set_unix_socket_path(md_http_t *http, const char *path)
+{
+ http->unix_socket_path = path;
+}
+
+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) {
+ 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_request_t *req;
+ apr_pool_t *pool;
+ apr_status_t rv;
+
+ rv = apr_pool_create(&pool, http->pool);
+ if (rv != APR_SUCCESS) {
+ return rv;
+ }
+ apr_pool_tag(pool, "md_http_req");
+
+ 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->user_agent = http->user_agent;
+ req->proxy_url = http->proxy_url;
+ req->timeout = http->timeout;
+ req->ca_file = http->ca_file;
+ req->unix_socket_path = http->unix_socket_path;
+ *preq = req;
+ return rv;
+}
+
+void md_http_req_destroy(md_http_request_t *req)
+{
+ if (req->internals) {
+ req->http->impl->req_cleanup(req);
+ req->internals = NULL;
+ }
+ apr_pool_destroy(req->pool);
+}
+
+void md_http_set_on_status_cb(md_http_request_t *req, md_http_status_cb *cb, void *baton)
+{
+ req->cb.on_status = cb;
+ req->cb.on_status_data = baton;
+}
+
+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;
+}
+
+apr_status_t md_http_perform(md_http_request_t *req)
+{
+ return req->http->impl->perform(req);
+}
+
+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)
+{
+ nextreq_proxy_t *proxy = baton;
+
+ return proxy->nextreq(preq, proxy->baton, http, in_flight);
+}
+
+apr_status_t md_http_multi_perform(md_http_t *http, md_http_next_req *nextreq, void *baton)
+{
+ nextreq_proxy_t proxy;
+
+ proxy.nextreq = nextreq;
+ proxy.baton = baton;
+ return http->impl->multi_perform(http, http->pool, proxy_nextreq, &proxy);
+}
+
+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, "GET", url, headers);
+ *preq = (APR_SUCCESS == rv)? req : NULL;
+ return rv;
+}
+
+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, "HEAD", url, headers);
+ *preq = (APR_SUCCESS == rv)? req : NULL;
+ return rv;
+}
+
+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;
+
+ 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;
+}
+
+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;
+
+ 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) {
+ *preq = req;
+ }
+ else {
+ *preq = NULL;
+ if (req) md_http_req_destroy(req);
+ }
+ 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
new file mode 100644
index 0000000..2f250f6
--- /dev/null
+++ b/modules/md/md_http.h
@@ -0,0 +1,272 @@
+/* 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_http_h
+#define mod_md_md_http_h
+
+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;
+
+/**
+ * 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;
+ const char *user_agent;
+ const char *proxy_url;
+ const char *ca_file;
+ const char *unix_socket_path;
+ apr_table_t *headers;
+ struct apr_bucket_brigade *body;
+ apr_off_t body_len;
+ apr_off_t resp_limit;
+ md_http_timeouts_t timeout;
+ md_http_callbacks_t cb;
+ void *internals;
+};
+
+struct md_http_response_t {
+ md_http_request_t *req;
+ int status;
+ apr_table_t *headers;
+ struct apr_bucket_brigade *body;
+};
+
+apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent,
+ const char *proxy_url);
+
+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
+ * 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);
+
+/**
+ * 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);
+
+/**
+ * Set a CA file (in PERM format) to use for root certificates when
+ * verifying SSL connections. If not set (or set to NULL), the systems
+ * certificate store will be used.
+ */
+void md_http_set_ca_file(md_http_t *http, const char *ca_file);
+
+/**
+ * Set the path of a unix domain socket for use instead of TCP
+ * in a connection. Disable by providing NULL as path.
+ */
+void md_http_set_unix_socket_path(md_http_t *http, const char *path);
+
+/**
+ * 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);
+
+/**
+ * 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 request.
+ * @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 request.
+ * @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 request 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 request 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);
+
+/*
+ * 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 requests 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_cleanup_cb(md_http_t *req, apr_pool_t *p);
+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;
+ md_http_cleanup_cb *cleanup;
+};
+
+void md_http_use_implementation(md_http_impl_t *impl);
+
+/**
+ * get/set data the implementation wants to remember between requests
+ * in the same md_http_t instance.
+ */
+void md_http_set_impl_data(md_http_t *http, void *data);
+void *md_http_get_impl_data(md_http_t *http);
+
+
+#endif /* md_http_h */
diff --git a/modules/md/md_json.c b/modules/md/md_json.c
new file mode 100644
index 0000000..e0f977e
--- /dev/null
+++ b/modules/md/md_json.c
@@ -0,0 +1,1311 @@
+/* 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_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
+ * when undefining their INLINEs, we get static, unused functions, arg
+ */
+#if defined(__GNUC__)
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)
+#pragma GCC diagnostic push
+#endif
+#pragma GCC diagnostic ignored "-Wunused-function"
+#pragma GCC diagnostic ignored "-Wunreachable-code"
+#elif defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-function"
+#endif
+
+#include <jansson_config.h>
+#undef JSON_INLINE
+#define JSON_INLINE
+#include <jansson.h>
+
+#if defined(__GNUC__)
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)
+#pragma GCC diagnostic pop
+#endif
+#elif defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+struct md_json_t {
+ apr_pool_t *p;
+ json_t *j;
+};
+
+/**************************************************************************************************/
+/* lifecycle */
+
+static apr_status_t json_pool_cleanup(void *data)
+{
+ md_json_t *json = data;
+ if (json) {
+ md_json_destroy(json);
+ }
+ return APR_SUCCESS;
+}
+
+static md_json_t *json_create(apr_pool_t *pool, json_t *j)
+{
+ md_json_t *json;
+
+ if (!j) {
+ apr_abortfunc_t abfn = apr_pool_abort_get(pool);
+ if (abfn) {
+ abfn(APR_ENOMEM);
+ }
+ assert(j != NULL); /* failsafe in case abort is unset */
+ }
+ json = apr_pcalloc(pool, sizeof(*json));
+ json->p = pool;
+ json->j = j;
+ apr_pool_cleanup_register(pool, json, json_pool_cleanup, apr_pool_cleanup_null);
+
+ return json;
+}
+
+md_json_t *md_json_create(apr_pool_t *pool)
+{
+ return json_create(pool, json_object());
+}
+
+md_json_t *md_json_create_s(apr_pool_t *pool, const char *s)
+{
+ return json_create(pool, json_string(s));
+}
+
+void md_json_destroy(md_json_t *json)
+{
+ if (json && json->j) {
+ assert(json->j->refcount > 0);
+ json_decref(json->j);
+ json->j = NULL;
+ }
+}
+
+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, const md_json_t *json)
+{
+ return json_create(pool, json_deep_copy(json->j));
+}
+
+/**************************************************************************************************/
+/* selectors */
+
+
+static json_t *jselect(const md_json_t *json, va_list ap)
+{
+ json_t *j;
+ const char *key;
+
+ j = json->j;
+ key = va_arg(ap, char *);
+ while (key && j) {
+ j = json_object_get(j, key);
+ key = va_arg(ap, char *);
+ }
+ return j;
+}
+
+static json_t *jselect_parent(const char **child_key, int create, md_json_t *json, va_list ap)
+{
+ const char *key, *next;
+ json_t *j, *jn;
+
+ *child_key = NULL;
+ j = json->j;
+ key = va_arg(ap, char *);
+ while (key && j) {
+ next = va_arg(ap, char *);
+ if (next) {
+ jn = json_object_get(j, key);
+ if (!jn && create) {
+ jn = json_object();
+ json_object_set_new(j, key, jn);
+ }
+ j = jn;
+ }
+ else {
+ *child_key = key;
+ }
+ key = next;
+ }
+ return j;
+}
+
+static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap)
+{
+ const char *key;
+ json_t *j, *aj;
+
+ j = jselect_parent(&key, 1, json, ap);
+
+ if (!j || !json_is_object(j)) {
+ return APR_EINVAL;
+ }
+
+ aj = json_object_get(j, key);
+ if (!aj) {
+ aj = json_array();
+ json_object_set_new(j, key, aj);
+ }
+
+ if (!json_is_array(aj)) {
+ return APR_EINVAL;
+ }
+
+ json_array_append(aj, val);
+ return APR_SUCCESS;
+}
+
+static apr_status_t jselect_insert(json_t *val, size_t index, md_json_t *json, va_list ap)
+{
+ const char *key;
+ json_t *j, *aj;
+
+ 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);
+ }
+
+ if (!json_is_array(aj)) {
+ json_decref(val);
+ return APR_EINVAL;
+ }
+
+ if (json_array_size(aj) <= index) {
+ json_array_append(aj, val);
+ }
+ else {
+ json_array_insert(aj, index, val);
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t jselect_set(json_t *val, md_json_t *json, va_list ap)
+{
+ const char *key;
+ json_t *j;
+
+ j = jselect_parent(&key, 1, json, ap);
+
+ if (!j) {
+ return APR_EINVAL;
+ }
+
+ if (key) {
+ if (!json_is_object(j)) {
+ return APR_EINVAL;
+ }
+ json_object_set(j, key, val);
+ }
+ else {
+ /* replace */
+ if (json->j) {
+ json_decref(json->j);
+ }
+ json_incref(val);
+ json->j = val;
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t jselect_set_new(json_t *val, md_json_t *json, va_list ap)
+{
+ const char *key;
+ json_t *j;
+
+ 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_new(j, key, val);
+ }
+ else {
+ /* replace */
+ if (json->j) {
+ json_decref(json->j);
+ }
+ json->j = val;
+ }
+ return APR_SUCCESS;
+}
+
+int md_json_has_key(const md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ return j != NULL;
+}
+
+/**************************************************************************************************/
+/* type things */
+
+int md_json_is(const md_json_type_t jtype, md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+ switch (jtype) {
+ case MD_JSON_TYPE_OBJECT: return (j && json_is_object(j));
+ case MD_JSON_TYPE_ARRAY: return (j && json_is_array(j));
+ case MD_JSON_TYPE_STRING: return (j && json_is_string(j));
+ case MD_JSON_TYPE_REAL: return (j && json_is_real(j));
+ case MD_JSON_TYPE_INT: return (j && json_is_integer(j));
+ case MD_JSON_TYPE_BOOL: return (j && (json_is_true(j) || json_is_false(j)));
+ case MD_JSON_TYPE_NULL: return (j == NULL);
+ }
+ 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 */
+
+int md_json_getb(const md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ return j? json_is_true(j) : 0;
+}
+
+apr_status_t md_json_setb(int value, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, json);
+ rv = jselect_set_new(json_boolean(value), json, ap);
+ va_end(ap);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* numbers */
+
+double md_json_getn(const md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+ return (j && json_is_number(j))? json_number_value(j) : 0.0;
+}
+
+apr_status_t md_json_setn(double value, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, json);
+ rv = jselect_set_new(json_real(value), json, ap);
+ va_end(ap);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* longs */
+
+long md_json_getl(const md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+ return (long)((j && json_is_number(j))? json_integer_value(j) : 0L);
+}
+
+apr_status_t md_json_setl(long value, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, json);
+ rv = jselect_set_new(json_integer(value), json, ap);
+ va_end(ap);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* strings */
+
+const char *md_json_gets(const md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ return (j && json_is_string(j))? json_string_value(j) : NULL;
+}
+
+const char *md_json_dups(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);
+
+ return (j && json_is_string(j))? apr_pstrdup(p, json_string_value(j)) : NULL;
+}
+
+apr_status_t md_json_sets(const char *value, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, json);
+ rv = jselect_set_new(json_string(value), json, ap);
+ va_end(ap);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* 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, ...)
+{
+ json_t *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (j) {
+ if (j == json->j) {
+ return json;
+ }
+ json_incref(j);
+ return json_create(json->p, j);
+ }
+ 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;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (j) {
+ if (j == json->j) {
+ return json;
+ }
+ json_incref(j);
+ return json_create(json->p, j);
+ }
+ return NULL;
+}
+
+apr_status_t md_json_setj(const md_json_t *value, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+ const char *key;
+ json_t *j;
+
+ if (value) {
+ va_start(ap, json);
+ rv = jselect_set(value->j, json, ap);
+ va_end(ap);
+ }
+ else {
+ va_start(ap, json);
+ j = jselect_parent(&key, 1, json, ap);
+ va_end(ap);
+
+ if (key && j && !json_is_object(j)) {
+ json_object_del(j, key);
+ rv = APR_SUCCESS;
+ }
+ else {
+ rv = APR_EINVAL;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, json);
+ rv = jselect_add(value->j, json, ap);
+ va_end(ap);
+ return rv;
+}
+
+apr_status_t md_json_insertj(md_json_t *value, size_t index, md_json_t *json, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, json);
+ rv = jselect_insert(value->j, index, json, ap);
+ va_end(ap);
+ 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 */
+
+apr_status_t md_json_clr(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_object(j)) {
+ json_object_clear(j);
+ }
+ else if (j && json_is_array(j)) {
+ json_array_clear(j);
+ }
+ return APR_SUCCESS;
+}
+
+apr_status_t md_json_del(md_json_t *json, ...)
+{
+ const char *key;
+ json_t *j;
+ va_list ap;
+
+ 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;
+}
+
+/**************************************************************************************************/
+/* object strings */
+
+apr_status_t md_json_gets_dict(apr_table_t *dict, 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_object(j)) {
+ const char *key;
+ json_t *val;
+
+ json_object_foreach(j, key, val) {
+ if (json_is_string(val)) {
+ apr_table_set(dict, key, json_string_value(val));
+ }
+ }
+ return APR_SUCCESS;
+ }
+ return APR_ENOENT;
+}
+
+static int object_set(void *data, const char *key, const char *val)
+{
+ json_t *j = data, *nj = json_string(val);
+ json_object_set(j, key, nj);
+ json_decref(nj);
+ return 1;
+}
+
+apr_status_t md_json_sets_dict(apr_table_t *dict, md_json_t *json, ...)
+{
+ json_t *nj, *j;
+ va_list ap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (!j || !json_is_object(j)) {
+ const char *key;
+
+ va_start(ap, json);
+ j = jselect_parent(&key, 1, json, ap);
+ va_end(ap);
+
+ if (!key || !j || !json_is_object(j)) {
+ return APR_EINVAL;
+ }
+ nj = json_object();
+ json_object_set_new(j, key, nj);
+ j = nj;
+ }
+
+ apr_table_do(object_set, j, dict, NULL);
+ return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* conversions */
+
+apr_status_t md_json_pass_to(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+ (void)p;
+ (void)baton;
+ return md_json_setj(value, json, NULL);
+}
+
+apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton)
+{
+ (void)p;
+ (void)baton;
+ *pvalue = json;
+ return APR_SUCCESS;
+}
+
+apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+ (void)baton;
+ return md_json_setj(md_json_clone(p, value), json, NULL);
+}
+
+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);
+ return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* array generic */
+
+apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, void *baton,
+ const md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+ apr_status_t rv = APR_SUCCESS;
+ size_t index;
+ json_t *val;
+ md_json_t wrap;
+ void *element;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (!j || !json_is_array(j)) {
+ return APR_ENOENT;
+ }
+
+ wrap.p = a->pool;
+ json_array_foreach(j, index, val) {
+ wrap.j = val;
+ if (APR_SUCCESS == (rv = cb(&element, &wrap, wrap.p, baton))) {
+ if (element) {
+ APR_ARRAY_PUSH(a, void*) = element;
+ }
+ }
+ else if (APR_ENOENT == rv) {
+ rv = APR_SUCCESS;
+ }
+ else {
+ break;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_json_seta(apr_array_header_t *a, md_json_to_cb *cb, void *baton,
+ md_json_t *json, ...)
+{
+ json_t *j, *nj;
+ md_json_t wrap;
+ apr_status_t rv = APR_SUCCESS;
+ va_list ap;
+ int i;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (!j || !json_is_array(j)) {
+ const char *key;
+
+ va_start(ap, json);
+ j = jselect_parent(&key, 1, json, ap);
+ va_end(ap);
+
+ if (!key || !j || !json_is_object(j)) {
+ return APR_EINVAL;
+ }
+ nj = json_array();
+ json_object_set_new(j, key, nj);
+ j = nj;
+ }
+
+ json_array_clear(j);
+ wrap.p = json->p;
+ for (i = 0; i < a->nelts; ++i) {
+ if (!cb) {
+ return APR_EINVAL;
+ }
+ wrap.j = json_string("");
+ if (APR_SUCCESS == (rv = cb(APR_ARRAY_IDX(a, i, void*), &wrap, json->p, baton))) {
+ json_array_append_new(j, wrap.j);
+ }
+ }
+ return rv;
+}
+
+int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+ size_t index;
+ json_t *val;
+ md_json_t wrap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (!j || !json_is_array(j)) {
+ return 0;
+ }
+
+ wrap.p = json->p;
+ json_array_foreach(j, index, val) {
+ wrap.j = val;
+ if (!cb(baton, index, &wrap)) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+int md_json_iterkey(md_json_iterkey_cb *cb, void *baton, md_json_t *json, ...)
+{
+ json_t *j;
+ va_list ap;
+ const char *key;
+ json_t *val;
+ md_json_t wrap;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (!j || !json_is_object(j)) {
+ return 0;
+ }
+
+ wrap.p = json->p;
+ json_object_foreach(j, key, val) {
+ wrap.j = val;
+ if (!cb(baton, key, &wrap)) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/**************************************************************************************************/
+/* array strings */
+
+apr_status_t md_json_getsa(apr_array_header_t *a, 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_array(j)) {
+ size_t index;
+ json_t *val;
+
+ json_array_foreach(j, index, val) {
+ if (json_is_string(val)) {
+ APR_ARRAY_PUSH(a, const char *) = json_string_value(val);
+ }
+ }
+ return APR_SUCCESS;
+ }
+ return APR_ENOENT;
+}
+
+apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, 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_array(j)) {
+ size_t index;
+ json_t *val;
+
+ apr_array_clear(a);
+ json_array_foreach(j, index, val) {
+ if (json_is_string(val)) {
+ APR_ARRAY_PUSH(a, const char *) = apr_pstrdup(p, json_string_value(val));
+ }
+ }
+ return APR_SUCCESS;
+ }
+ return APR_ENOENT;
+}
+
+apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...)
+{
+ json_t *nj, *j;
+ va_list ap;
+ int i;
+
+ va_start(ap, json);
+ j = jselect(json, ap);
+ va_end(ap);
+
+ if (!j || !json_is_array(j)) {
+ const char *key;
+
+ va_start(ap, json);
+ j = jselect_parent(&key, 1, json, ap);
+ va_end(ap);
+
+ if (!key || !j || !json_is_object(j)) {
+ return APR_EINVAL;
+ }
+ nj = json_array();
+ json_object_set_new(j, key, nj);
+ j = nj;
+ }
+
+ json_array_clear(j);
+ for (i = 0; i < a->nelts; ++i) {
+ json_array_append_new(j, json_string(APR_ARRAY_IDX(a, i, const char*)));
+ }
+ return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* formatting, parsing */
+
+typedef struct {
+ const md_json_t *json;
+ md_json_fmt_t fmt;
+ const char *fname;
+ apr_file_t *f;
+} j_write_ctx;
+
+/* Convert from md_json_fmt_t to the Jansson json_dumpX flags. */
+static size_t fmt_to_flags(md_json_fmt_t fmt)
+{
+ /* NOTE: JSON_PRESERVE_ORDER is off by default before Jansson 2.8. It
+ * doesn't have any semantic effect on the protocol, but it does let the
+ * md_json_writeX unit tests run deterministically. */
+ return JSON_PRESERVE_ORDER |
+ ((fmt == MD_JSON_FMT_COMPACT) ? JSON_COMPACT : JSON_INDENT(2));
+}
+
+static int dump_cb(const char *buffer, size_t len, void *baton)
+{
+ apr_bucket_brigade *bb = baton;
+ apr_status_t rv;
+
+ rv = apr_brigade_write(bb, NULL, NULL, buffer, len);
+ return (rv == APR_SUCCESS)? 0 : -1;
+}
+
+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;
+}
+
+static int chunk_cb(const char *buffer, size_t len, void *baton)
+{
+ apr_array_header_t *chunks = baton;
+ 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(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt)
+{
+ apr_array_header_t *chunks;
+ int rv;
+
+ chunks = apr_array_make(p, 10, sizeof(char *));
+ rv = json_dump_callback(json->j, chunk_cb, chunks, fmt_to_flags(fmt));
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "md_json_writep failed to dump JSON");
+ return NULL;
+ }
+
+ switch (chunks->nelts) {
+ case 0:
+ return "";
+ case 1:
+ return APR_ARRAY_IDX(chunks, 0, const char*);
+ default:
+ return apr_array_pstrcat(p, chunks, 0);
+ }
+}
+
+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;
+
+ if ((s = md_json_writep(json, p, fmt))) {
+ rv = apr_file_write_full(f, s, strlen(s), NULL);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef: error writing file");
+ }
+ }
+ else {
+ rv = APR_EINVAL;
+ 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(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;
+ apr_file_t *f;
+
+ rv = md_util_fcreatex(&f, fpath, perms, p);
+ if (APR_SUCCESS == rv) {
+ rv = md_json_writef(json, p, fmt, f);
+ apr_file_close(f);
+ }
+ return rv;
+}
+
+static apr_status_t write_json(void *baton, apr_file_t *f, apr_pool_t *p)
+{
+ j_write_ctx *ctx = baton;
+ apr_status_t rv = md_json_writef(ctx->json, p, ctx->fmt, f);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "freplace json in %s", ctx->fname);
+ }
+ return rv;
+}
+
+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;
+ ctx.json = json;
+ ctx.fmt = fmt;
+ ctx.fname = fpath;
+ return md_util_freplace(fpath, perms, p, write_json, &ctx);
+}
+
+apr_status_t md_json_readd(md_json_t **pjson, apr_pool_t *pool, const char *data, size_t data_len)
+{
+ json_error_t error;
+ json_t *j;
+
+ j = json_loadb(data, data_len, 0, &error);
+ if (!j) {
+ return APR_EINVAL;
+ }
+ *pjson = json_create(pool, j);
+ return APR_SUCCESS;
+}
+
+static size_t load_cb(void *data, size_t max_len, void *baton)
+{
+ apr_bucket_brigade *body = baton;
+ size_t blen, read_len = 0;
+ const char *bdata;
+ char *dest = data;
+ apr_bucket *b;
+ apr_status_t rv;
+
+ while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) {
+ b = APR_BRIGADE_FIRST(body);
+ if (APR_BUCKET_IS_METADATA(b)) {
+ if (APR_BUCKET_IS_EOS(b)) {
+ body = NULL;
+ }
+ }
+ else {
+ rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ);
+ if (rv == APR_SUCCESS) {
+ if (blen > max_len) {
+ apr_bucket_split(b, max_len);
+ blen = max_len;
+ }
+ memcpy(dest, bdata, blen);
+ read_len += blen;
+ max_len -= blen;
+ dest += blen;
+ }
+ else {
+ body = NULL;
+ if (!APR_STATUS_IS_EOF(rv)) {
+ /* everything beside EOF is an error */
+ read_len = (size_t)-1;
+ }
+ }
+ }
+ APR_BUCKET_REMOVE(b);
+ apr_bucket_delete(b);
+ }
+
+ return read_len;
+}
+
+apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, apr_bucket_brigade *bb)
+{
+ json_error_t error;
+ json_t *j;
+
+ j = json_load_callback(load_cb, bb, 0, &error);
+ if (j) {
+ *pjson = json_create(pool, j);
+ } else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, pool,
+ "failed to load JSON file: %s (line %d:%d)",
+ error.text, error.line, error.column);
+ }
+ return (j && *pjson) ? APR_SUCCESS : APR_EINVAL;
+}
+
+static size_t load_file_cb(void *data, size_t max_len, void *baton)
+{
+ apr_file_t *f = baton;
+ apr_size_t len = max_len;
+ apr_status_t rv;
+
+ rv = apr_file_read(f, data, &len);
+ if (APR_SUCCESS == rv) {
+ return len;
+ }
+ else if (APR_EOF == rv) {
+ return 0;
+ }
+ return (size_t)-1;
+}
+
+apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *p, const char *fpath)
+{
+ apr_file_t *f;
+ json_t *j;
+ apr_status_t rv;
+ json_error_t error;
+
+ rv = apr_file_open(&f, fpath, APR_FOPEN_READ, 0, p);
+ if (rv != APR_SUCCESS) {
+ return rv;
+ }
+
+ j = json_load_callback(load_file_cb, f, 0, &error);
+ if (j) {
+ *pjson = json_create(p, j);
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "failed to load JSON file %s: %s (line %d:%d)",
+ fpath, error.text, error.line, error.column);
+ }
+
+ apr_file_close(f);
+ return (j && *pjson) ? APR_SUCCESS : APR_EINVAL;
+}
+
+/**************************************************************************************************/
+/* http get */
+
+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;
+ const char *ctype, *p;
+
+ *pjson = NULL;
+ if (!res->body) goto cleanup;
+ ctype = md_util_parse_ct(res->req->pool, apr_table_get(res->headers, "content-type"));
+ if (!ctype) goto cleanup;
+ p = ctype + strlen(ctype) +1;
+ if (!strcmp(p - sizeof("/json"), "/json")
+ || !strcmp(p - sizeof("+json"), "+json")) {
+ rv = md_json_readb(pjson, pool, res->body);
+ }
+cleanup:
+ return rv;
+}
+
+typedef struct {
+ apr_status_t rv;
+ apr_pool_t *pool;
+ md_json_t *json;
+} resp_data;
+
+static apr_status_t json_resp_cb(const md_http_response_t *res, void *data)
+{
+ resp_data *resp = data;
+ return md_json_read_http(&resp->json, resp->pool, res);
+}
+
+apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool,
+ struct md_http_t *http, const char *url)
+{
+ apr_status_t rv;
+ resp_data resp;
+
+ memset(&resp, 0, sizeof(resp));
+ resp.pool = pool;
+
+ rv = md_http_GET_perform(http, url, NULL, json_resp_cb, &resp);
+
+ if (rv == APR_SUCCESS) {
+ *pjson = resp.json;
+ return resp.rv;
+ }
+ *pjson = NULL;
+ return rv;
+}
+
+
+apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...)
+{
+ json_t *j;
+ va_list ap;
+ apr_status_t rv = APR_SUCCESS;
+
+ va_start(ap, src);
+ j = jselect(src, ap);
+ va_end(ap);
+
+ if (j) {
+ va_start(ap, src);
+ rv = jselect_set(j, dest, ap);
+ va_end(ap);
+ }
+ 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
new file mode 100644
index 0000000..50b8828
--- /dev/null
+++ b/modules/md/md_json.h
@@ -0,0 +1,157 @@
+/* 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_json_h
+#define mod_md_md_json_h
+
+#include <apr_file_io.h>
+
+struct apr_bucket_brigade;
+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;
+
+typedef enum {
+ MD_JSON_TYPE_OBJECT,
+ MD_JSON_TYPE_ARRAY,
+ MD_JSON_TYPE_STRING,
+ MD_JSON_TYPE_REAL,
+ MD_JSON_TYPE_INT,
+ MD_JSON_TYPE_BOOL,
+ MD_JSON_TYPE_NULL,
+} md_json_type_t;
+
+
+typedef enum {
+ MD_JSON_FMT_COMPACT,
+ MD_JSON_FMT_INDENT,
+} md_json_fmt_t;
+
+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, 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, ...);
+int md_json_is(const md_json_type_t type, md_json_t *json, ...);
+
+/* boolean manipulation */
+int md_json_getb(const md_json_t *json, ...);
+apr_status_t md_json_setb(int value, md_json_t *json, ...);
+
+/* number manipulation */
+double md_json_getn(const md_json_t *json, ...);
+apr_status_t md_json_setn(double value, md_json_t *json, ...);
+
+/* long manipulation */
+long md_json_getl(const md_json_t *json, ...);
+apr_status_t md_json_setl(long value, md_json_t *json, ...);
+
+/* string manipulation */
+md_json_t *md_json_create_s(apr_pool_t *pool, const char *s);
+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(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);
+
+/* identity pass through from json to json */
+apr_status_t md_json_pass_to(void *value, md_json_t *json, apr_pool_t *p, void *baton);
+apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton);
+
+/* 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, 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,
+ void *baton, const md_json_t *json, ...);
+apr_status_t md_json_seta(apr_array_header_t *a, md_json_to_cb *cb,
+ void *baton, md_json_t *json, ...);
+
+/* Called on each array element, aborts iteration when returning 0 */
+typedef int md_json_itera_cb(void *baton, size_t index, md_json_t *json);
+int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...);
+
+/* Called on each object key, aborts iteration when returning 0 */
+typedef int md_json_iterkey_cb(void *baton, const char* key, md_json_t *json);
+int md_json_iterkey(md_json_iterkey_cb *cb, void *baton, md_json_t *json, ...);
+
+/* Manipulating Object String values */
+apr_status_t md_json_gets_dict(apr_table_t *dict, const md_json_t *json, ...);
+apr_status_t md_json_sets_dict(apr_table_t *dict, md_json_t *json, ...);
+
+/* Manipulating String Arrays */
+apr_status_t md_json_getsa(apr_array_header_t *a, const md_json_t *json, ...);
+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(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(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(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);
+apr_status_t md_json_readd(md_json_t **pjson, apr_pool_t *pool, const char *data, size_t data_len);
+apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *pool, const char *fpath);
+
+
+/* http retrieval */
+apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool,
+ struct md_http_t *http, const char *url);
+apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool,
+ const struct md_http_response_t *res);
+
+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
new file mode 100644
index 0000000..c0e8c1b
--- /dev/null
+++ b/modules/md/md_jws.c
@@ -0,0 +1,148 @@
+/* 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 <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+#include <apr_buckets.h>
+
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_log.h"
+#include "md_util.h"
+
+apr_status_t md_jws_get_jwk(md_json_t **pjwk, apr_pool_t *p, struct md_pkey_t *pkey)
+{
+ md_json_t *jwk;
+
+ if (!pkey) return APR_EINVAL;
+
+ jwk = md_json_create(p);
+ md_json_sets(md_pkey_get_rsa_e64(pkey, p), jwk, "e", NULL);
+ md_json_sets("RSA", jwk, "kty", NULL);
+ md_json_sets(md_pkey_get_rsa_n64(pkey, p), jwk, "n", NULL);
+ *pjwk = jwk;
+ return APR_SUCCESS;
+}
+
+apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p,
+ md_data_t *payload, md_json_t *prot_fields,
+ struct md_pkey_t *pkey, const char *key_id)
+{
+ md_json_t *msg, *jprotected, *jwk;
+ const char *prot64, *pay64, *sign64, *sign, *prot;
+ md_data_t data;
+ apr_status_t rv;
+
+ msg = md_json_create(p);
+ jprotected = md_json_clone(p, prot_fields);
+ md_json_sets("RS256", jprotected, "alg", NULL);
+ if (key_id) {
+ md_json_sets(key_id, jprotected, "kid", NULL);
+ }
+ else {
+ rv = md_jws_get_jwk(&jwk, p, pkey);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "get jwk");
+ goto cleanup;
+ }
+ md_json_setj(jwk, jprotected, "jwk", NULL);
+ }
+
+ prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT);
+ if (!prot) {
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "serialize protected");
+ goto cleanup;
+ }
+
+ md_data_init(&data, prot, strlen(prot));
+ prot64 = md_util_base64url_encode(&data, p);
+ md_json_sets(prot64, msg, "protected", NULL);
+
+ pay64 = md_util_base64url_encode(payload, p);
+ md_json_sets(pay64, msg, "payload", NULL);
+ sign = apr_psprintf(p, "%s.%s", prot64, pay64);
+
+ rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign));
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message");
+ goto cleanup;
+ }
+ md_json_sets(sign64, msg, "signature", NULL);
+
+cleanup:
+ *pmsg = (APR_SUCCESS == rv)? msg : NULL;
+ return rv;
+}
+
+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_t data;
+ apr_status_t rv;
+
+ e64 = md_pkey_get_rsa_e64(pkey, p);
+ n64 = md_pkey_get_rsa_n64(pkey, p);
+ if (!e64 || !n64) {
+ return APR_EINVAL;
+ }
+
+ /* whitespace and order is relevant, since we hand out a digest of this */
+ s = apr_psprintf(p, "{\"e\":\"%s\",\"kty\":\"RSA\",\"n\":\"%s\"}", e64, n64);
+ md_data_init_str(&data, s);
+ rv = md_crypt_sha256_digest64(pthumb, p, &data);
+ return rv;
+}
+
+apr_status_t md_jws_hmac(md_json_t **pmsg, apr_pool_t *p,
+ md_data_t *payload, md_json_t *prot_fields,
+ const md_data_t *hmac_key)
+{
+ md_json_t *msg, *jprotected;
+ const char *prot64, *pay64, *mac64, *sign, *prot;
+ md_data_t data;
+ apr_status_t rv;
+
+ msg = md_json_create(p);
+ jprotected = md_json_clone(p, prot_fields);
+ md_json_sets("HS256", jprotected, "alg", NULL);
+ prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT);
+ if (!prot) {
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "serialize protected");
+ goto cleanup;
+ }
+
+ md_data_init(&data, prot, strlen(prot));
+ prot64 = md_util_base64url_encode(&data, p);
+ md_json_sets(prot64, msg, "protected", NULL);
+
+ pay64 = md_util_base64url_encode(payload, p);
+ md_json_sets(pay64, msg, "payload", NULL);
+ sign = apr_psprintf(p, "%s.%s", prot64, pay64);
+
+ rv = md_crypt_hmac64(&mac64, hmac_key, p, sign, strlen(sign));
+ if (APR_SUCCESS != rv) {
+ goto cleanup;
+ }
+ md_json_sets(mac64, msg, "signature", NULL);
+
+cleanup:
+ *pmsg = (APR_SUCCESS == rv)? msg : NULL;
+ return rv;
+}
diff --git a/modules/md/md_jws.h b/modules/md/md_jws.h
new file mode 100644
index 0000000..466f2df
--- /dev/null
+++ b/modules/md/md_jws.h
@@ -0,0 +1,52 @@
+/* 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_jws_h
+#define mod_md_md_jws_h
+
+struct apr_table_t;
+struct md_json_t;
+struct md_pkey_t;
+struct md_data_t;
+
+/**
+ * Get the JSON value of the 'jwk' field for the given key.
+ */
+apr_status_t md_jws_get_jwk(md_json_t **pjwk, apr_pool_t *p, struct md_pkey_t *pkey);
+
+/**
+ * Get the JWS key signed JSON message with given payload and protected fields, signed
+ * using the given key and optional key_id.
+ */
+apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p,
+ struct md_data_t *payload, md_json_t *prot_fields,
+ struct md_pkey_t *pkey, const char *key_id);
+/**
+ * Get the 'Thumbprint' as defined in RFC8555 for the given key in
+ * base64 encoding.
+ */
+apr_status_t md_jws_pkey_thumb(const char **pthumb64, apr_pool_t *p, struct md_pkey_t *pkey);
+
+/**
+ * Get the JWS HS256 signed message for given payload and protected fields,
+ * using the base64 encoded MAC key.
+ */
+apr_status_t md_jws_hmac(md_json_t **pmsg, apr_pool_t *p,
+ struct md_data_t *payload, md_json_t *prot_fields,
+ const struct md_data_t *hmac_key);
+
+
+#endif /* md_jws_h */
diff --git a/modules/md/md_log.c b/modules/md/md_log.c
new file mode 100644
index 0000000..d236e0f
--- /dev/null
+++ b/modules/md/md_log.c
@@ -0,0 +1,78 @@
+/* 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 <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#include "md_log.h"
+
+#define LOG_BUFFER_LEN 1024
+
+static const char *level_names[] = {
+ "emergency",
+ "alert",
+ "crit",
+ "err",
+ "warning",
+ "notice",
+ "info",
+ "debug",
+ "trace1",
+ "trace2",
+ "trace3",
+ "trace4",
+ "trace5",
+ "trace6",
+ "trace7",
+ "trace8",
+};
+
+const char *md_log_level_name(md_log_level_t level)
+{
+ return level_names[level];
+}
+
+static md_log_print_cb *log_printv;
+static md_log_level_cb *log_level;
+static void *log_baton;
+
+void md_log_set(md_log_level_cb *level_cb, md_log_print_cb *print_cb, void *baton)
+{
+ log_printv = print_cb;
+ log_level = level_cb;
+ log_baton = baton;
+}
+
+int md_log_is_level(apr_pool_t *p, md_log_level_t level)
+{
+ if (!log_level) {
+ return 0;
+ }
+ return log_level(log_baton, p, level);
+}
+
+void md_log_perror(const char *file, int line, md_log_level_t level,
+ apr_status_t rv, apr_pool_t *p, const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ if (log_printv) {
+ log_printv(file, line, level, rv, log_baton, p, fmt, ap);
+ }
+ va_end(ap);
+}
diff --git a/modules/md/md_log.h b/modules/md/md_log.h
new file mode 100644
index 0000000..19e688f
--- /dev/null
+++ b/modules/md/md_log.h
@@ -0,0 +1,60 @@
+/* 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_log_h
+#define mod_md_md_log_h
+
+typedef enum {
+ MD_LOG_EMERG,
+ MD_LOG_ALERT,
+ MD_LOG_CRIT,
+ MD_LOG_ERR,
+ MD_LOG_WARNING,
+ MD_LOG_NOTICE,
+ MD_LOG_INFO,
+ MD_LOG_DEBUG,
+ MD_LOG_TRACE1,
+ MD_LOG_TRACE2,
+ MD_LOG_TRACE3,
+ MD_LOG_TRACE4,
+ MD_LOG_TRACE5,
+ MD_LOG_TRACE6,
+ MD_LOG_TRACE7,
+ MD_LOG_TRACE8,
+} md_log_level_t;
+
+#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);
+
+void md_log_perror(const char *file, int line, md_log_level_t level,
+ apr_status_t rv, apr_pool_t *p, const char *fmt, ...)
+ __attribute__((format(printf,6,7)));
+
+typedef int md_log_level_cb(void *baton, apr_pool_t *p, md_log_level_t level);
+
+typedef void md_log_print_cb(const char *file, int line, md_log_level_t level,
+ apr_status_t rv, void *baton, apr_pool_t *p, const char *fmt, va_list ap);
+
+void md_log_set(md_log_level_cb *level_cb, md_log_print_cb *print_cb, void *baton);
+
+#endif /* md_log_h */
diff --git a/modules/md/md_ocsp.c b/modules/md/md_ocsp.c
new file mode 100644
index 0000000..8cbf05b
--- /dev/null
+++ b/modules/md/md_ocsp.c
@@ -0,0 +1,1063 @@
+/* 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>
+
+#if defined(LIBRESSL_VERSION_NUMBER)
+/* Missing from LibreSSL */
+#define MD_USE_OPENSSL_PRE_1_1_API (LIBRESSL_VERSION_NUMBER < 0x2070000f)
+#else /* defined(LIBRESSL_VERSION_NUMBER) */
+#define MD_USE_OPENSSL_PRE_1_1_API (OPENSSL_VERSION_NUMBER < 0x10100000L)
+#endif
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_event.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 *id_by_external_id;
+ apr_hash_t *ostat_by_id;
+ apr_thread_mutex_t *mutex;
+ md_timeslice_t renew_window;
+ md_job_notify_cb *notify;
+ void *notify_ctx;
+ apr_time_t min_delay;
+};
+
+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;
+};
+
+typedef struct md_ocsp_id_map_t md_ocsp_id_map_t;
+struct md_ocsp_id_map_t {
+ md_data_t id;
+ md_data_t external_id;
+};
+
+static void md_openssl_free(void *d)
+{
+ OPENSSL_free(d);
+}
+
+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;
+}
+
+apr_status_t md_ocsp_init_id(md_data_t *id, apr_pool_t *p, const md_cert_t *cert)
+{
+ unsigned char iddata[SHA_DIGEST_LENGTH];
+ X509 *x = md_cert_get_X509(cert);
+ unsigned int ulen = 0;
+
+ md_data_null(id);
+ if (X509_digest(x, EVP_sha1(), iddata, &ulen) != 1) {
+ return APR_EGENERAL;
+ }
+ md_data_assign_pcopy(id, (const char*)iddata, ulen, p);
+ 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;
+ }
+ md_data_clear(&ostat->req_der);
+}
+
+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;
+ }
+ md_data_clear(&ostat->resp_der);
+ 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;
+
+ rv = md_data_assign_copy(&ostat->resp_der, der->data, der->len);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ ostat->resp_stat = stat;
+ 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;
+
+cleanup:
+ 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 cleanup;
+ 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;
+cleanup:
+ 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 cleanup;
+ rv = md_store_load_json(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, &jprops, ptemp);
+ if (APR_SUCCESS != rv) goto cleanup;
+ rv = ostat_from_json(&resp_stat, &resp_der, &resp_valid, jprops, ptemp);
+ if (APR_SUCCESS != rv) goto cleanup;
+ rv = ostat_set(ostat, resp_stat, &resp_der, &resp_valid, mtime);
+ if (APR_SUCCESS != rv) goto cleanup;
+cleanup:
+ 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 cleanup;
+ mtime = md_store_get_modified(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, ptemp);
+ if (mtime) ostat->resp_mtime = mtime;
+cleanup:
+ 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->ostat_by_id);
+ 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,
+ apr_time_t min_delay)
+{
+ md_ocsp_reg_t *reg;
+ apr_status_t rv = APR_SUCCESS;
+
+ reg = apr_palloc(p, sizeof(*reg));
+ if (!reg) {
+ rv = APR_ENOMEM;
+ goto cleanup;
+ }
+ reg->p = p;
+ reg->store = store;
+ reg->user_agent = user_agent;
+ reg->proxy_url = proxy_url;
+ reg->id_by_external_id = apr_hash_make(p);
+ reg->ostat_by_id = apr_hash_make(p);
+ reg->renew_window = *renew_window;
+ reg->min_delay = min_delay;
+
+ rv = apr_thread_mutex_create(&reg->mutex, APR_THREAD_MUTEX_NESTED, p);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ apr_pool_cleanup_register(p, reg, ocsp_reg_cleanup, apr_pool_cleanup_null);
+cleanup:
+ *preg = (APR_SUCCESS == rv)? reg : NULL;
+ return rv;
+}
+
+apr_status_t md_ocsp_prime(md_ocsp_reg_t *reg, const char *ext_id, apr_size_t ext_id_len,
+ md_cert_t *cert, md_cert_t *issuer, const md_t *md)
+{
+ md_ocsp_status_t *ostat;
+ const char *name;
+ md_data_t id;
+ apr_status_t rv = APR_SUCCESS;
+
+ /* Called during post_config. no mutex protection needed */
+ name = md? md->name : MD_OTHER;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, reg->p,
+ "md[%s]: priming OCSP status", name);
+
+ rv = md_ocsp_init_id(&id, reg->p, cert);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ ostat = apr_hash_get(reg->ostat_by_id, id.data, (apr_ssize_t)id.len);
+ if (ostat) goto cleanup; /* already seen it, cert is used in >1 server_rec */
+
+ ostat = apr_pcalloc(reg->p, sizeof(*ostat));
+ ostat->id = id;
+ 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 cleanup;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p,
+ "md[%s]: getting ocsp responder from cert", name);
+ rv = md_cert_get_ocsp_responder_url(&ostat->responder_url, reg->p, cert);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, reg->p,
+ "md[%s]: certificate with serial %s has no OCSP responder URL",
+ name, md_cert_get_serial_number(cert, reg->p));
+ goto cleanup;
+ }
+
+ 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 cleanup;
+ }
+
+ /* 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->ostat_by_id, ostat->id.data, (apr_ssize_t)ostat->id.len, ostat);
+ if (ext_id) {
+ md_ocsp_id_map_t *id_map;
+
+ id_map = apr_pcalloc(reg->p, sizeof(*id_map));
+ id_map->id = id;
+ md_data_assign_pcopy(&id_map->external_id, ext_id, ext_id_len, reg->p);
+ /* check for collision/uniqness? */
+ apr_hash_set(reg->id_by_external_id, id_map->external_id.data,
+ (apr_ssize_t)id_map->external_id.len, id_map);
+ }
+ rv = APR_SUCCESS;
+cleanup:
+ return rv;
+}
+
+apr_status_t md_ocsp_get_status(md_ocsp_copy_der *cb, void *userdata, md_ocsp_reg_t *reg,
+ const char *ext_id, apr_size_t ext_id_len,
+ apr_pool_t *p, const md_t *md)
+{
+ md_ocsp_status_t *ostat;
+ const char *name;
+ apr_status_t rv = APR_SUCCESS;
+ md_ocsp_id_map_t *id_map;
+ const char *id;
+ apr_size_t id_len;
+ int locked = 0;
+
+ (void)p;
+ (void)md;
+ name = md? md->name : MD_OTHER;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p,
+ "md[%s]: OCSP, get_status", name);
+
+ id_map = apr_hash_get(reg->id_by_external_id, ext_id, (apr_ssize_t)ext_id_len);
+ id = id_map? id_map->id.data : ext_id;
+ id_len = id_map? id_map->id.len : ext_id_len;
+ ostat = apr_hash_get(reg->ostat_by_id, id, (apr_ssize_t)id_len);
+ if (!ostat) {
+ rv = APR_ENOENT;
+ goto cleanup;
+ }
+
+ /* 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);
+ cb(NULL, 0, userdata);
+ goto cleanup;
+ }
+ }
+ /* 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);
+ }
+ }
+
+ cb((const unsigned char*)ostat->resp_der.data, ostat->resp_der.len, userdata);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p,
+ "md[%s]: OCSP, provided %ld bytes of response",
+ name, (long)ostat->resp_der.len);
+cleanup:
+ 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 response 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)
+{
+ 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;
+ 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 = md_ocsp_init_id(&id, p, cert);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ ostat = apr_hash_get(reg->ostat_by_id, id.data, (apr_ssize_t)id.len);
+ if (!ostat) {
+ rv = APR_ENOENT;
+ goto cleanup;
+ }
+ ocsp_get_meta(&stat, &valid, reg, ostat, p);
+cleanup:
+ *pstat = stat;
+ *pvalid = valid;
+ return rv;
+}
+
+apr_size_t md_ocsp_count(md_ocsp_reg_t *reg)
+{
+ return apr_hash_count(reg->ostat_by_id);
+}
+
+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);
+ der.free_data = md_openssl_free;
+ md_data_to_hex(&hex, 0, p, &der);
+ md_data_clear(&der);
+ 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;
+
+#if MD_USE_OPENSSL_PRE_1_1_API
+ certid = resp->certId;
+#else
+ certid = OCSP_SINGLERESP_get0_id(resp);
+#endif
+ 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 cleanup;
+ }
+ 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,
+ apr_psprintf(req->pool, "req[%d] response body does not parse as "
+ "OCSP response, status=%d, body brigade length=%ld",
+ resp->req->id, resp->status, (long)der.len));
+ md_result_log(update->result, MD_LOG_DEBUG);
+ goto cleanup;
+ }
+ /* 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 cleanup;
+ }
+ 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 cleanup;
+ }
+ /* 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_TRACE3, 0, req->pool,
+ "req[%d]: OCSP response 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 cleanup;
+
+ case -1:
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool,
+ "req[%d]: OCSP response 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 cleanup;
+ }
+ 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 cleanup;
+ }
+ 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 cleanup;
+ }
+
+ /* 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 cleanup;
+ }
+ new_der.len = (apr_size_t)n;
+ new_der.free_data = md_openssl_free;
+ nstat = (bstatus == V_OCSP_CERTSTATUS_GOOD)? MD_OCSP_CERT_ST_GOOD : MD_OCSP_CERT_ST_REVOKED;
+ 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 cleanup;
+ }
+
+ 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);
+
+cleanup:
+ md_data_clear(&new_der);
+ 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(update->job, ostat->errors, NULL);
+ 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_event_holler("ocsp-errored", update->job->mdomain, update->job, update->result, update->p);
+ goto cleanup;
+ }
+ md_event_holler("ocsp-renewed", update->job->mdomain, update->job, update->result, update->p);
+
+cleanup:
+ 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 ocsp_req_make(OCSP_REQUEST **pocsp_req, OCSP_CERTID *certid)
+{
+ OCSP_REQUEST *req = NULL;
+ OCSP_CERTID *id_copy = NULL;
+ apr_status_t rv = APR_ENOMEM;
+
+ req = OCSP_REQUEST_new();
+ if (!req) goto cleanup;
+ id_copy = OCSP_CERTID_dup(certid);
+ if (!id_copy) goto cleanup;
+ if (!OCSP_request_add0_id(req, id_copy)) goto cleanup;
+ id_copy = NULL;
+ OCSP_request_add1_nonce(req, 0, -1);
+ rv = APR_SUCCESS;
+cleanup:
+ if (id_copy) OCSP_CERTID_free(id_copy);
+ if (APR_SUCCESS != rv && req) {
+ OCSP_REQUEST_free(req);
+ req = NULL;
+ }
+ *pocsp_req = req;
+ return rv;
+}
+
+static apr_status_t ocsp_req_assign_der(md_data_t *d, OCSP_REQUEST *ocsp_req)
+{
+ int len;
+
+ md_data_clear(d);
+ len = i2d_OCSP_REQUEST(ocsp_req, (unsigned char**)&d->data);
+ if (len < 0) return APR_ENOMEM;
+ d->len = (apr_size_t)len;
+ d->free_data = md_openssl_free;
+ return APR_SUCCESS;
+}
+
+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;
+ md_http_request_t *req = NULL;
+ apr_status_t rv = APR_ENOENT;
+ apr_table_t *headers;
+
+ 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) {
+ rv = ocsp_req_make(&ostat->ocsp_req, ostat->certid);
+ if (APR_SUCCESS != rv) goto cleanup;
+ }
+ if (0 == ostat->req_der.len) {
+ rv = ocsp_req_assign_der(&ostat->req_der, ostat->ocsp_req);
+ if (APR_SUCCESS != rv) goto cleanup;
+ }
+ 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 cleanup;
+ 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;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->pool,
+ "scheduling OCSP request[%d] for %s, %d request in flight",
+ req->id, ostat->md_name, in_flight);
+ }
+ }
+cleanup:
+ *preq = (APR_SUCCESS == rv)? req : NULL;
+ 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->ostat_by_id);
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p,
+ "OCSP status updates due: %d", ctx.todos->nelts);
+ if (!ctx.todos->nelts) goto cleanup;
+
+ rv = md_http_create(&http, ptemp, reg->user_agent, reg->proxy_url);
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ rv = md_http_multi_perform(http, next_todo, &ctx);
+
+cleanup:
+ /* 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->ostat_by_id);
+
+ /* 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 && APR_ENOENT != 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->ostat_by_id);
+
+ 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, &reg->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->ostat_by_id), sizeof(md_ocsp_status_t*));
+ json = md_json_create(p);
+
+ apr_hash_do(add_ostat, &ctx, reg->ostat_by_id);
+ 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;
+}
+
+md_job_t *md_ocsp_job_make(md_ocsp_reg_t *ocsp, const char *mdomain, apr_pool_t *p)
+{
+ return md_job_make(p, ocsp->store, MD_SG_OCSP, mdomain, ocsp->min_delay);
+}
diff --git a/modules/md/md_ocsp.h b/modules/md/md_ocsp.h
new file mode 100644
index 0000000..c91dc54
--- /dev/null
+++ b/modules/md/md_ocsp.h
@@ -0,0 +1,71 @@
+/* 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_data_t;
+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_time_t min_delay);
+
+apr_status_t md_ocsp_init_id(struct md_data_t *id, apr_pool_t *p, const md_cert_t *cert);
+
+apr_status_t md_ocsp_prime(md_ocsp_reg_t *reg, const char *ext_id, apr_size_t ext_id_len,
+ md_cert_t *x, md_cert_t *issuer, const md_t *md);
+
+typedef void md_ocsp_copy_der(const unsigned char *der, apr_size_t der_len, void *userdata);
+
+apr_status_t md_ocsp_get_status(md_ocsp_copy_der *cb, void *userdata, md_ocsp_reg_t *reg,
+ const char *ext_id, apr_size_t ext_id_len,
+ 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);
+
+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
new file mode 100644
index 0000000..8bceb0e
--- /dev/null
+++ b/modules/md/md_reg.c
@@ -0,0 +1,1323 @@
+/* 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 <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_event.h"
+#include "md_log.h"
+#include "md_json.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_status.h"
+#include "md_tailscale.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+
+struct md_reg_t {
+ apr_pool_t *p;
+ struct md_store_t *store;
+ struct apr_hash_t *protos;
+ struct apr_hash_t *certs;
+ int can_http;
+ int can_https;
+ const char *proxy_url;
+ const char *ca_file;
+ int domains_frozen;
+ md_timeslice_t *renew_window;
+ md_timeslice_t *warn_window;
+ md_job_notify_cb *notify;
+ void *notify_ctx;
+ apr_time_t min_delay;
+ int retry_failover;
+ int use_store_locks;
+ apr_time_t lock_wait_timeout;
+};
+
+/**************************************************************************************************/
+/* life cycle */
+
+static apr_status_t load_props(md_reg_t *reg, apr_pool_t *p)
+{
+ md_json_t *json;
+ apr_status_t rv;
+
+ rv = md_store_load(reg->store, MD_SG_NONE, NULL, MD_FN_HTTPD_JSON,
+ MD_SV_JSON, (void**)&json, p);
+ if (APR_SUCCESS == rv) {
+ if (md_json_has_key(json, MD_KEY_PROTO, MD_KEY_HTTP, NULL)) {
+ reg->can_http = md_json_getb(json, MD_KEY_PROTO, MD_KEY_HTTP, NULL);
+ }
+ if (md_json_has_key(json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL)) {
+ reg->can_https = md_json_getb(json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL);
+ }
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ }
+ return rv;
+}
+
+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,
+ int use_store_locks, apr_time_t lock_wait_timeout)
+{
+ md_reg_t *reg;
+ apr_status_t rv;
+
+ reg = apr_pcalloc(p, sizeof(*reg));
+ reg->p = p;
+ reg->store = store;
+ reg->protos = apr_hash_make(p);
+ reg->certs = apr_hash_make(p);
+ reg->can_http = 1;
+ reg->can_https = 1;
+ reg->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
+ reg->ca_file = (ca_file && apr_strnatcasecmp("none", ca_file))?
+ 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);
+
+ if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))
+ && APR_SUCCESS == (rv = md_tailscale_protos_add(reg->protos, p))) {
+ rv = load_props(reg, p);
+ }
+
+ *preg = (rv == APR_SUCCESS)? reg : NULL;
+ return rv;
+}
+
+struct md_store_t *md_reg_store_get(md_reg_t *reg)
+{
+ return reg->store;
+}
+
+/**************************************************************************************************/
+/* checks */
+
+static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, int fields)
+{
+ apr_status_t rv = APR_SUCCESS;
+ const char *err = NULL;
+
+ if (MD_UPD_DOMAINS & fields) {
+ const md_t *other;
+ const char *domain;
+ int i;
+
+ if (!md->domains || md->domains->nelts <= 0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, APR_EINVAL, p,
+ "empty domain list: %s", md->name);
+ return APR_EINVAL;
+ }
+
+ for (i = 0; i < md->domains->nelts; ++i) {
+ domain = APR_ARRAY_IDX(md->domains, i, const char *);
+ if (!md_dns_is_name(p, domain, 1) && !md_dns_is_wildcard(p, domain)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p,
+ "md %s with invalid domain name: %s", md->name, domain);
+ return APR_EINVAL;
+ }
+ }
+
+ if (NULL != (other = md_reg_find_overlap(reg, md, &domain, p))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p,
+ "md %s shares domain '%s' with md %s",
+ md->name, domain, other->name);
+ return APR_EINVAL;
+ }
+ }
+
+ if (MD_UPD_CONTACTS & fields) {
+ const char *contact;
+ int i;
+
+ for (i = 0; i < md->contacts->nelts && !err; ++i) {
+ contact = APR_ARRAY_IDX(md->contacts, i, const char *);
+ rv = md_util_abs_uri_check(p, contact, &err);
+
+ if (err) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p,
+ "contact for %s invalid (%s): %s", md->name, err, contact);
+ return APR_EINVAL;
+ }
+ }
+ }
+
+ if ((MD_UPD_CA_URL & fields) && md->ca_urls) { /* setting to empty is ok */
+ int i;
+ const char *url;
+ for (i = 0; i < md->ca_urls->nelts; ++i) {
+ url = APR_ARRAY_IDX(md->ca_urls, i, const char*);
+ rv = md_util_abs_uri_check(p, url, &err);
+ if (err) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p,
+ "CA url for %s invalid (%s): %s", md->name, err, url);
+ return APR_EINVAL;
+ }
+ }
+ }
+
+ if ((MD_UPD_CA_PROTO & fields) && md->ca_proto) { /* setting to empty is ok */
+ /* Do we want to restrict this to "known" protocols? */
+ }
+
+ if ((MD_UPD_CA_ACCOUNT & fields) && md->ca_account) { /* setting to empty is ok */
+ /* hmm, in case we know the protocol, some checks could be done */
+ }
+
+ if ((MD_UPD_AGREEMENT & fields) && md->ca_agreement
+ && strcmp("accepted", md->ca_agreement)) { /* setting to empty is ok */
+ rv = md_util_abs_uri_check(p, md->ca_agreement, &err);
+ if (err) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p,
+ "CA url for %s invalid (%s): %s", md->name, err, md->ca_agreement);
+ return APR_EINVAL;
+ }
+ }
+
+ return rv;
+}
+
+/**************************************************************************************************/
+/* state assessment */
+
+static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md)
+{
+ md_state_t state = MD_S_COMPLETE;
+ const char *state_descr = NULL;
+ const md_pubcert_t *pub;
+ const md_cert_t *cert;
+ const md_pkey_spec_t *spec;
+ apr_status_t rv = APR_SUCCESS;
+ int i;
+
+ if (md->renew_window == NULL) md->renew_window = reg->renew_window;
+ if (md->warn_window == NULL) md->warn_window = reg->warn_window;
+
+ if (md->domains && md->domains->pool != p) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+ "md{%s}: state_init called with foreign pool", md->name);
+ }
+
+ for (i = 0; i < md_cert_count(md); ++i) {
+ spec = md_pkeys_spec_get(md->pks, i);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p,
+ "md{%s}: check cert %s", md->name, md_pkey_spec_name(spec));
+ rv = md_reg_get_pubcert(&pub, reg, md, i, p);
+ if (APR_SUCCESS == rv) {
+ cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*);
+ if (!md_is_covered_by_alt_names(md, pub->alt_names)) {
+ state = MD_S_INCOMPLETE;
+ state_descr = apr_psprintf(p, "certificate(%s) does not cover all domains.",
+ md_pkey_spec_name(spec));
+ goto cleanup;
+ }
+ if (!md->must_staple != !md_cert_must_staple(cert)) {
+ state = MD_S_INCOMPLETE;
+ state_descr = apr_psprintf(p, "'must-staple' is%s requested, but "
+ "certificate(%s) has it%s enabled.",
+ md->must_staple? "" : " not",
+ md_pkey_spec_name(spec),
+ !md->must_staple? "" : " not");
+ goto cleanup;
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: certificate(%d) is ok",
+ md->name, i);
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ state = MD_S_INCOMPLETE;
+ state_descr = apr_psprintf(p, "certificate(%s) is missing",
+ md_pkey_spec_name(spec));
+ rv = APR_SUCCESS;
+ goto cleanup;
+ }
+ else {
+ state = MD_S_ERROR;
+ state_descr = "error initializing";
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name);
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "md{%s}: state=%d, %s",
+ md->name, state, state_descr);
+ md->state = state;
+ md->state_descr = state_descr;
+ return rv;
+}
+
+/**************************************************************************************************/
+/* iteration */
+
+typedef struct {
+ md_reg_t *reg;
+ md_reg_do_cb *cb;
+ void *baton;
+ const char *exclude;
+ const void *result;
+} reg_do_ctx;
+
+static int reg_md_iter(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp)
+{
+ reg_do_ctx *ctx = baton;
+
+ (void)store;
+ if (!ctx->exclude || strcmp(ctx->exclude, md->name)) {
+ state_init(ctx->reg, ptemp, (md_t*)md);
+ return ctx->cb(ctx->baton, ctx->reg, md);
+ }
+ return 1;
+}
+
+static int reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p, const char *exclude)
+{
+ reg_do_ctx ctx;
+
+ ctx.reg = reg;
+ ctx.cb = cb;
+ ctx.baton = baton;
+ ctx.exclude = exclude;
+ return md_store_md_iter(reg_md_iter, &ctx, reg->store, p, MD_SG_DOMAINS, "*");
+}
+
+
+int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p)
+{
+ return reg_do(cb, baton, reg, p, NULL);
+}
+
+/**************************************************************************************************/
+/* lookup */
+
+md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p)
+{
+ md_t *md;
+
+ if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, name, &md, p)) {
+ state_init(reg, p, md);
+ return md;
+ }
+ return NULL;
+}
+
+typedef struct {
+ const char *domain;
+ md_t *md;
+} find_domain_ctx;
+
+static int find_domain(void *baton, md_reg_t *reg, md_t *md)
+{
+ find_domain_ctx *ctx = baton;
+
+ (void)reg;
+ if (md_contains(md, ctx->domain, 0)) {
+ ctx->md = md;
+ return 0;
+ }
+ return 1;
+}
+
+md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p)
+{
+ find_domain_ctx ctx;
+
+ ctx.domain = domain;
+ ctx.md = NULL;
+
+ md_reg_do(find_domain, &ctx, reg, p);
+ if (ctx.md) {
+ state_init(reg, p, ctx.md);
+ }
+ return ctx.md;
+}
+
+typedef struct {
+ const md_t *md_checked;
+ md_t *md;
+ const char *s;
+} find_overlap_ctx;
+
+static int find_overlap(void *baton, md_reg_t *reg, md_t *md)
+{
+ find_overlap_ctx *ctx = baton;
+ const char *overlap;
+
+ (void)reg;
+ if ((overlap = md_common_name(ctx->md_checked, md))) {
+ ctx->md = md;
+ ctx->s = overlap;
+ return 0;
+ }
+ return 1;
+}
+
+md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, apr_pool_t *p)
+{
+ find_overlap_ctx ctx;
+
+ ctx.md_checked = md;
+ ctx.md = NULL;
+ ctx.s = NULL;
+
+ reg_do(find_overlap, &ctx, reg, p, md->name);
+ if (pdomain && ctx.s) {
+ *pdomain = ctx.s;
+ }
+ if (ctx.md) {
+ state_init(reg, p, ctx.md);
+ }
+ return ctx.md;
+}
+
+/**************************************************************************************************/
+/* manipulation */
+
+static apr_status_t p_md_add(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_reg_t *reg = baton;
+ apr_status_t rv = APR_SUCCESS;
+ md_t *md, *mine;
+ int do_check;
+
+ md = va_arg(ap, md_t *);
+ do_check = va_arg(ap, int);
+
+ if (reg->domains_frozen) return APR_EACCES;
+ mine = md_clone(ptemp, md);
+ if (do_check && APR_SUCCESS != (rv = check_values(reg, ptemp, md, MD_UPD_ALL))) goto leave;
+ if (APR_SUCCESS != (rv = state_init(reg, ptemp, mine))) goto leave;
+ rv = md_save(reg->store, p, MD_SG_DOMAINS, mine, 1);
+leave:
+ return rv;
+}
+
+static apr_status_t add_md(md_reg_t *reg, md_t *md, apr_pool_t *p, int do_checks)
+{
+ return md_util_pool_vdo(p_md_add, reg, p, md, do_checks, NULL);
+}
+
+apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p)
+{
+ return add_md(reg, md, p, 1);
+}
+
+static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_reg_t *reg = baton;
+ apr_status_t rv = APR_SUCCESS;
+ const char *name;
+ const md_t *md, *updates;
+ int fields, do_checks;
+ md_t *nmd;
+
+ name = va_arg(ap, const char *);
+ updates = va_arg(ap, const md_t *);
+ fields = va_arg(ap, int);
+ do_checks = va_arg(ap, int);
+
+ if (NULL == (md = md_reg_get(reg, name, ptemp))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, APR_ENOENT, ptemp, "md %s", name);
+ return APR_ENOENT;
+ }
+
+ 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;
+ }
+
+ if (reg->domains_frozen) return APR_EACCES;
+ nmd = md_copy(ptemp, md);
+ if (MD_UPD_DOMAINS & fields) {
+ nmd->domains = updates->domains;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update domains: %s", name);
+ }
+ if (MD_UPD_CA_URL & fields) {
+ nmd->ca_urls = (updates->ca_urls?
+ apr_array_copy(p, updates->ca_urls) : NULL);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca url: %s", name);
+ }
+ if (MD_UPD_CA_PROTO & fields) {
+ nmd->ca_proto = updates->ca_proto;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca protocol: %s", name);
+ }
+ if (MD_UPD_CA_ACCOUNT & fields) {
+ nmd->ca_account = updates->ca_account;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update account: %s", name);
+ }
+ if (MD_UPD_CONTACTS & fields) {
+ nmd->contacts = updates->contacts;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update contacts: %s", name);
+ }
+ if (MD_UPD_AGREEMENT & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update agreement: %s", name);
+ nmd->ca_agreement = updates->ca_agreement;
+ }
+ if (MD_UPD_DRIVE_MODE & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update drive-mode: %s", name);
+ nmd->renew_mode = updates->renew_mode;
+ }
+ 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;
+ }
+ 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;
+ }
+ if (MD_UPD_CA_CHALLENGES & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca challenges: %s", name);
+ nmd->ca_challenges = (updates->ca_challenges?
+ apr_array_copy(p, updates->ca_challenges) : NULL);
+ }
+ if (MD_UPD_PKEY_SPEC & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update pkey spec: %s", name);
+ nmd->pks = md_pkeys_spec_clone(p, updates->pks);
+ }
+ if (MD_UPD_REQUIRE_HTTPS & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update require-https: %s", name);
+ nmd->require_https = updates->require_https;
+ }
+ if (MD_UPD_TRANSITIVE & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update transitive: %s", name);
+ nmd->transitive = updates->transitive;
+ }
+ if (MD_UPD_MUST_STAPLE & fields) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update must-staple: %s", name);
+ nmd->must_staple = updates->must_staple;
+ }
+ if (MD_UPD_PROTO & fields) {
+ 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);
+ }
+ return rv;
+}
+
+apr_status_t md_reg_update(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_delete_acct(md_reg_t *reg, apr_pool_t *p, const char *acct_id)
+{
+ apr_status_t rv = APR_SUCCESS;
+
+ rv = md_store_remove(reg->store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCOUNT, p, 1);
+ if (APR_SUCCESS == rv) {
+ md_store_remove(reg->store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1);
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* certificate related */
+
+static apr_status_t pubcert_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_reg_t *reg = baton;
+ apr_array_header_t *certs;
+ md_pubcert_t *pubcert, **ppubcert;
+ const md_t *md;
+ int index;
+ const md_cert_t *cert;
+ md_cert_state_t cert_state;
+ md_store_group_t group;
+ apr_status_t rv;
+
+ ppubcert = va_arg(ap, md_pubcert_t **);
+ group = (md_store_group_t)va_arg(ap, int);
+ md = va_arg(ap, const md_t *);
+ index = va_arg(ap, int);
+
+ if (md->cert_files && md->cert_files->nelts) {
+ rv = md_chain_fload(&certs, p, APR_ARRAY_IDX(md->cert_files, index, const char *));
+ }
+ else {
+ md_pkey_spec_t *spec = md_pkeys_spec_get(md->pks, index);;
+ rv = md_pubcert_load(reg->store, group, md->name, spec, &certs, p);
+ }
+ if (APR_SUCCESS != rv) goto leave;
+ if (certs->nelts == 0) {
+ rv = APR_ENOENT;
+ goto leave;
+ }
+
+ pubcert = apr_pcalloc(p, sizeof(*pubcert));
+ pubcert->certs = certs;
+ cert = APR_ARRAY_IDX(certs, 0, const md_cert_t *);
+ if (APR_SUCCESS != (rv = md_cert_get_alt_names(&pubcert->alt_names, cert, p))) goto leave;
+ switch ((cert_state = md_cert_state_get(cert))) {
+ case MD_CERT_VALID:
+ case MD_CERT_EXPIRED:
+ break;
+ default:
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, ptemp,
+ "md %s has unexpected cert state: %d", md->name, cert_state);
+ rv = APR_ENOTIMPL;
+ break;
+ }
+leave:
+ *ppubcert = (APR_SUCCESS == rv)? pubcert : NULL;
+ return rv;
+}
+
+apr_status_t md_reg_get_pubcert(const md_pubcert_t **ppubcert, md_reg_t *reg,
+ const md_t *md, int i, apr_pool_t *p)
+{
+ apr_status_t rv = APR_SUCCESS;
+ const md_pubcert_t *pubcert;
+ const char *name;
+
+ name = apr_psprintf(p, "%s[%d]", md->name, i);
+ pubcert = apr_hash_get(reg->certs, name, (apr_ssize_t)strlen(name));
+ if (!pubcert && !reg->domains_frozen) {
+ rv = md_util_pool_vdo(pubcert_load, reg, reg->p, &pubcert, MD_SG_DOMAINS, md, i, NULL);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ /* We cache it missing with an empty record */
+ pubcert = apr_pcalloc(reg->p, sizeof(*pubcert));
+ }
+ else if (APR_SUCCESS != rv) goto leave;
+ if (p != reg->p) name = apr_pstrdup(reg->p, name);
+ apr_hash_set(reg->certs, name, (apr_ssize_t)strlen(name), pubcert);
+ }
+leave:
+ if (APR_SUCCESS == rv && (!pubcert || !pubcert->certs)) {
+ rv = APR_ENOENT;
+ }
+ *ppubcert = (APR_SUCCESS == rv)? pubcert : NULL;
+ return rv;
+}
+
+apr_status_t md_reg_get_cred_files(const char **pkeyfile, const char **pcertfile,
+ md_reg_t *reg, md_store_group_t group,
+ const md_t *md, md_pkey_spec_t *spec, apr_pool_t *p)
+{
+ apr_status_t rv;
+
+ rv = md_store_get_fname(pkeyfile, reg->store, group, md->name, md_pkey_filename(spec, p), p);
+ if (APR_SUCCESS != rv) return rv;
+ if (!md_file_exists(*pkeyfile, p)) return APR_ENOENT;
+ rv = md_store_get_fname(pcertfile, reg->store, group, md->name, md_chain_filename(spec, p), p);
+ if (APR_SUCCESS != rv) return rv;
+ if (!md_file_exists(*pcertfile, p)) return APR_ENOENT;
+ return APR_SUCCESS;
+}
+
+apr_time_t md_reg_valid_until(md_reg_t *reg, const md_t *md, apr_pool_t *p)
+{
+ const md_pubcert_t *pub;
+ const md_cert_t *cert;
+ int i;
+ apr_time_t t, valid_until = 0;
+ apr_status_t rv;
+
+ for (i = 0; i < md_cert_count(md); ++i) {
+ rv = md_reg_get_pubcert(&pub, reg, md, i, p);
+ if (APR_SUCCESS == rv) {
+ cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*);
+ t = md_cert_get_not_after(cert);
+ if (valid_until == 0 || t < valid_until) {
+ valid_until = t;
+ }
+ }
+ }
+ return valid_until;
+}
+
+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;
+ int i;
+ apr_time_t renew_at = 0;
+ apr_status_t rv;
+
+ if (md->state == MD_S_INCOMPLETE) return apr_time_now();
+ for (i = 0; i < md_cert_count(md); ++i) {
+ rv = md_reg_get_pubcert(&pub, reg, md, i, p);
+ 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);
+ certlife.end = md_cert_get_not_after(cert);
+
+ 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_TRACE2, 0, p,
+ "md[%s]: certificate(%d) valid[%s] renewal[%s]",
+ md->name, i,
+ md_timeperiod_print(p, &certlife),
+ md_timeperiod_print(p, &renewal));
+ }
+
+ if (renew_at == 0 || renewal.start < renew_at) {
+ renew_at = renewal.start;
+ }
+ }
+ }
+ return renew_at;
+}
+
+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;
+ const md_cert_t *cert;
+ md_timeperiod_t certlife, warn;
+ int i;
+ apr_status_t rv;
+
+ if (md->state == MD_S_INCOMPLETE) return 0;
+ for (i = 0; i < md_cert_count(md); ++i) {
+ rv = md_reg_get_pubcert(&pub, reg, md, i, p);
+ if (APR_STATUS_IS_ENOENT(rv)) return 0;
+ if (APR_SUCCESS == rv) {
+ cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*);
+ certlife.start = md_cert_get_not_before(cert);
+ certlife.end = md_cert_get_not_after(cert);
+
+ 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_TRACE2, 0, p,
+ "md[%s]: certificate(%d) life[%s] warn[%s]",
+ md->name, i,
+ md_timeperiod_print(p, &certlife),
+ md_timeperiod_print(p, &warn));
+ }
+ if (md_timeperiod_has_started(&warn, apr_time_now())) {
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+/**************************************************************************************************/
+/* syncing */
+
+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) {
+ md_json_t *json;
+
+ if (reg->domains_frozen) return APR_EACCES;
+ reg->can_http = can_http;
+ reg->can_https = can_https;
+
+ json = md_json_create(p);
+ md_json_setb(can_http, json, MD_KEY_PROTO, MD_KEY_HTTP, NULL);
+ md_json_setb(can_https, json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL);
+
+ return md_store_save(reg->store, p, MD_SG_NONE, NULL, MD_FN_HTTPD_JSON, MD_SV_JSON, json, 0);
+ }
+ return APR_SUCCESS;
+}
+
+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_start(md_reg_t *reg, apr_array_header_t *master_mds, apr_pool_t *p)
+{
+ sync_ctx_v2 ctx;
+ apr_status_t rv;
+ md_t *md, *oldmd;
+ const char *name;
+ int i, idx;
+
+ 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;
+ }
+ }
+
+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 syncing 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_proto_t *proto;
+
+ if (!md->ca_proto) {
+ md->ca_proto = MD_PROTO_ACME;
+ }
+ proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto));
+ if (!proto) {
+ rv = APR_ENOTIMPL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp,
+ "[%s] uses unknown CA protocol '%s'",
+ md->name, md->ca_proto);
+ goto leave;
+ }
+ rv = proto->complete_md(md, p);
+ if (APR_SUCCESS != rv) goto leave;
+
+ rv = state_init(reg, p, md);
+ if (APR_SUCCESS != rv) goto leave;
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "loading md %s", md->name);
+ if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, md->name, &old, ptemp)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "loaded md %s", md->name);
+ /* 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_effective && old->ca_effective) {
+ md->ca_effective = apr_pstrdup(p, old->ca_effective);
+ }
+ 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_array_str_eq(md->ca_urls, old->ca_urls, 0)
+ && !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_pkeys_spec_eq(md->pks, old->pks)
+ && !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->cert_files, old->cert_files, 0)
+ && md_array_str_eq(md->pkey_files, old->pkey_files, 0)
+ && md_array_str_eq(md->ca_challenges, old->ca_challenges, 0)) {
+ changed = 0;
+ }
+ }
+ if (changed) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "saving md %s", md->name);
+ 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;
+}
+
+apr_status_t md_reg_remove(md_reg_t *reg, apr_pool_t *p, const char *name, int archive)
+{
+ if (reg->domains_frozen) return APR_EACCES;
+ return md_store_move(reg->store, p, MD_SG_DOMAINS, MD_SG_ARCHIVE, name, archive);
+}
+
+typedef struct {
+ md_reg_t *reg;
+ apr_pool_t *p;
+ apr_array_header_t *mds;
+} cleanup_challenge_ctx;
+
+static apr_status_t cleanup_challenge_inspector(void *baton, const char *dir, const char *name,
+ md_store_vtype_t vtype, void *value,
+ apr_pool_t *ptemp)
+{
+ cleanup_challenge_ctx *ctx = baton;
+ const md_t *md;
+ int i, used;
+ apr_status_t rv;
+
+ (void)value;
+ (void)vtype;
+ (void)dir;
+ for (used = 0, i = 0; i < ctx->mds->nelts && !used; ++i) {
+ md = APR_ARRAY_IDX(ctx->mds, i, const md_t *);
+ used = !strcmp(name, md->name);
+ }
+ if (!used) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp,
+ "challenges/%s: not in use, purging", name);
+ rv = md_store_purge(ctx->reg->store, ctx->p, MD_SG_CHALLENGES, name);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ptemp,
+ "challenges/%s: unable to purge", name);
+ }
+ }
+ return APR_SUCCESS;
+}
+
+apr_status_t md_reg_cleanup_challenges(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp,
+ apr_array_header_t *mds)
+{
+ apr_status_t rv;
+ cleanup_challenge_ctx ctx;
+
+ (void)p;
+ ctx.reg = reg;
+ ctx.p = ptemp;
+ ctx.mds = mds;
+ rv = md_store_iter_names(cleanup_challenge_inspector, &ctx, reg->store, ptemp,
+ MD_SG_CHALLENGES, "*");
+ return rv;
+}
+
+
+/**************************************************************************************************/
+/* driving */
+
+static apr_status_t run_init(void *baton, apr_pool_t *p, ...)
+{
+ va_list ap;
+ md_reg_t *reg = baton;
+ const md_t *md;
+ 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);
+
+ *pdriver = driver = apr_pcalloc(p, sizeof(*driver));
+
+ driver->p = p;
+ driver->env = env? apr_table_copy(p, env) : apr_table_make(p, 10);
+ driver->reg = reg;
+ driver->store = md_reg_store_get(reg);
+ driver->proxy_url = reg->proxy_url;
+ driver->ca_file = reg->ca_file;
+ 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 = 0;
+ }
+
+ if (!md->ca_proto) {
+ md_result_printf(result, APR_EGENERAL, "CA protocol is not defined");
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md[%s]: %s", md->name, result->detail);
+ goto leave;
+ }
+
+ driver->proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto));
+ if (!driver->proto) {
+ md_result_printf(result, APR_EGENERAL, "Unknown CA protocol '%s'", md->ca_proto);
+ goto leave;
+ }
+
+ if (preload) {
+ result->status = driver->proto->init_preload(driver, result);
+ }
+ else {
+ result->status = driver->proto->init(driver, result);
+ }
+
+leave:
+ if (APR_SUCCESS != result->status) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, result->status, p, "md[%s]: %s", md->name,
+ result->detail? result->detail : "<see error log for details>");
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: init done", md->name);
+ }
+ return result->status;
+}
+
+static apr_status_t run_test_init(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ const md_t *md;
+ apr_table_t *env;
+ md_result_t *result;
+ md_proto_driver_t *driver;
+
+ (void)p;
+ md = va_arg(ap, const md_t *);
+ env = va_arg(ap, apr_table_t *);
+ result = va_arg(ap, md_result_t *);
+
+ 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,
+ md_result_t *result, apr_pool_t *p)
+{
+ return md_util_pool_vdo(run_test_init, reg, p, md, env, result, NULL);
+}
+
+static apr_status_t run_renew(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_reg_t *reg = baton;
+ const md_t *md;
+ int reset, attempt;
+ md_proto_driver_t *driver;
+ apr_table_t *env;
+ apr_status_t rv;
+ md_result_t *result;
+
+ (void)p;
+ md = va_arg(ap, const md_t *);
+ env = va_arg(ap, apr_table_t *);
+ reset = va_arg(ap, int);
+ attempt = va_arg(ap, int);
+ result = va_arg(ap, md_result_t *);
+
+ rv = run_init(reg, 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;
+ driver->attempt = attempt;
+ driver->retry_failover = reg->retry_failover;
+ rv = driver->proto->renew(driver, result);
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: staging done", md->name);
+ return rv;
+}
+
+apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, apr_table_t *env,
+ int reset, int attempt,
+ md_result_t *result, apr_pool_t *p)
+{
+ return md_util_pool_vdo(run_renew, reg, p, md, env, reset, attempt, result, NULL);
+}
+
+static apr_status_t run_load_staging(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_reg_t *reg = baton;
+ const md_t *md;
+ md_proto_driver_t *driver;
+ md_result_t *result;
+ apr_table_t *env;
+ md_job_t *job;
+ apr_status_t rv;
+
+ /* For the MD, check if something is in the STAGING area. If none is there,
+ * return that status. Otherwise ask the protocol driver to preload it into
+ * a new, temporary area.
+ * If that succeeds, we move the TEMP area over the DOMAINS (causing the
+ * existing one go to ARCHIVE).
+ * Finally, we clean up the data from CHALLENGES and STAGING.
+ */
+ md = va_arg(ap, const md_t*);
+ env = va_arg(ap, apr_table_t*);
+ 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, 0, ptemp, "%s: nothing staged", md->name);
+ goto out;
+ }
+
+ 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);
+ md_result_activity_setn(result, "preloading staged to tmp");
+ rv = driver->proto->preload(driver, MD_SG_TMP, result);
+ if (APR_SUCCESS != rv) goto out;
+
+ /* If we had a job saved in STAGING, copy it over too */
+ 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 */
+ md_result_activity_setn(result, "moving tmp to become new domains");
+ rv = md_store_move(reg->store, p, MD_SG_TMP, MD_SG_DOMAINS, md->name, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, NULL);
+ goto out;
+ }
+
+ 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_event_holler("installed", md->name, job, result, ptemp);
+ if (job->dirty) md_job_save(job, result, ptemp);
+
+out:
+ if (!APR_STATUS_IS_ENOENT(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "%s: load done", md->name);
+ }
+ return rv;
+}
+
+apr_status_t md_reg_load_staging(md_reg_t *reg, const md_t *md, apr_table_t *env,
+ md_result_t *result, apr_pool_t *p)
+{
+ if (reg->domains_frozen) return APR_EACCES;
+ 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;
+ md_t *md;
+ const md_pubcert_t *pubcert;
+ int i, j;
+
+ assert(!reg->domains_frozen);
+ /* prefill the certs cache for all mds */
+ for (i = 0; i < mds->nelts; ++i) {
+ md = APR_ARRAY_IDX(mds, i, md_t*);
+ for (j = 0; j < md_cert_count(md); ++j) {
+ rv = md_reg_get_pubcert(&pubcert, reg, md, i, reg->p);
+ if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) goto leave;
+ }
+ }
+ reg->domains_frozen = 1;
+leave:
+ return rv;
+}
+
+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->warn_window = *warn_window;
+}
+
+md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p)
+{
+ return md_job_make(p, reg->store, MD_SG_STAGING, mdomain, reg->min_delay);
+}
diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h
new file mode 100644
index 0000000..58ee16a
--- /dev/null
+++ b/modules/md/md_reg.h
@@ -0,0 +1,313 @@
+/* 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_reg_h
+#define mod_md_md_reg_h
+
+struct apr_hash_t;
+struct apr_array_header_t;
+struct md_pkey_t;
+struct md_cert_t;
+struct md_result_t;
+struct md_pkey_spec_t;
+
+#include "md_store.h"
+
+/**
+ * A registry for managed domains with a md_store_t as persistence.
+ *
+ */
+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,
+ int use_store_locks, apr_time_t lock_wait_timeout);
+
+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);
+
+/**
+ * Add a new md to the registry. This will check the name for uniqueness and
+ * that domain names do not overlap with already existing mds.
+ */
+apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p);
+
+/**
+ * Find the md, if any, that contains the given domain name.
+ * NULL if none found.
+ */
+md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p);
+
+/**
+ * Find one md, which domain names overlap with the given md and that has a different
+ * name. There may be more than one existing md that overlaps. It is not defined
+ * which one will be returned.
+ */
+md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, apr_pool_t *p);
+
+/**
+ * Get the md with the given unique name. NULL if it does not exist.
+ * Will update the md->state.
+ */
+md_t *md_reg_get(md_reg_t *reg, const char *name, 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);
+
+/**
+ * Invoke callback for all mds in this registry. Order is not guaranteed.
+ * If the callback returns 0, iteration stops. Returns 0 if iteration was
+ * aborted.
+ */
+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 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
+
+/**
+ * Update the given fields for the managed domain. Take the new
+ * 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, int check_consistency);
+
+/**
+ * Get the chain of public certificates of the managed domain md, starting with the cert
+ * of the domain and going up the issuers. Returns APR_ENOENT when not available.
+ */
+apr_status_t md_reg_get_pubcert(const md_pubcert_t **ppubcert, md_reg_t *reg,
+ const md_t *md, int i, apr_pool_t *p);
+
+/**
+ * Get the filenames of private key and pubcert of the MD - if they exist.
+ * @return APR_ENOENT if one or both do not exist.
+ */
+apr_status_t md_reg_get_cred_files(const char **pkeyfile, const char **pcertfile,
+ md_reg_t *reg, md_store_group_t group,
+ const md_t *md, struct md_pkey_spec_t *spec, apr_pool_t *p);
+
+/**
+ * Synchronize the given master mds with the store.
+ */
+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);
+
+/**
+ * Delete the account from the local store.
+ */
+apr_status_t md_reg_delete_acct(md_reg_t *reg, apr_pool_t *p, const char *acct_id);
+
+
+/**
+ * Cleanup any challenges that are no longer in use.
+ *
+ * @param reg the registry
+ * @param p pool for permanent storage
+ * @param ptemp pool for temporary storage
+ * @param mds the list of configured MDs
+ */
+apr_status_t md_reg_cleanup_challenges(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp,
+ apr_array_header_t *mds);
+
+/**
+ * Mark all information from group MD_SG_DOMAINS as readonly, deny future modifications
+ * (MD_SG_STAGING and MD_SG_CHALLENGES remain writeable). For the given MDs, cache
+ * the public information (MDs themselves and their pubcerts or lack of).
+ */
+apr_status_t md_reg_freeze_domains(md_reg_t *reg, apr_array_header_t *mds);
+
+/**
+ * Return if the certificate of the MD should be renewed. This includes reaching
+ * the renewal window of an otherwise valid certificate. It return also !0 iff
+ * no certificate has been obtained yet.
+ */
+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 the timestamp up to which *all* certificates for the MD can be used.
+ * A value of 0 indicates that there is no certificate.
+ */
+apr_time_t md_reg_valid_until(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.
+ */
+int md_reg_should_warn(md_reg_t *reg, const md_t *md, apr_pool_t *p);
+
+/**************************************************************************************************/
+/* protocol drivers */
+
+typedef struct md_proto_t md_proto_t;
+
+typedef struct md_proto_driver_t md_proto_driver_t;
+
+/**
+ * Operating environment for a protocol driver. This is valid only for the
+ * duration of one run (init + renew, init + preload).
+ */
+struct md_proto_driver_t {
+ const md_proto_t *proto;
+ apr_pool_t *p;
+ void *baton;
+ struct apr_table_t *env;
+
+ md_reg_t *reg;
+ md_store_t *store;
+ const char *proxy_url;
+ const char *ca_file;
+ const md_t *md;
+
+ int can_http;
+ int can_https;
+ int reset;
+ int attempt;
+ int retry_failover;
+ 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);
+typedef apr_status_t md_proto_complete_md_cb(md_t *md, apr_pool_t *p);
+
+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;
+ md_proto_complete_md_cb *complete_md;
+};
+
+/**
+ * Run a test initialization of the renew protocol for the given MD. This verifies
+ * basic parameter settings and is expected to return a description of encountered
+ * problems in <pmessage> when != APR_SUCCESS.
+ * A message return is allocated fromt the given pool.
+ */
+apr_status_t md_reg_test_init(md_reg_t *reg, const md_t *md, struct apr_table_t *env,
+ struct md_result_t *result, apr_pool_t *p);
+
+/**
+ * Obtain new credentials for the given managed domain in STAGING.
+ * @param reg the registry instance
+ * @param md the mdomain to renew
+ * @param env global environment of settings
+ * @param reset != 0 if any previous, partial information should be wiped
+ * @param attempt the number of attempts made this far (for this md)
+ * @param result for reporting results of the renewal
+ * @param p the memory pool to use
+ * @return APR_SUCCESS if new credentials have been staged successfully
+ */
+apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md,
+ struct apr_table_t *env, int reset, int attempt,
+ struct md_result_t *result, apr_pool_t *p);
+
+/**
+ * Load a new set of credentials for the managed domain from STAGING - if it exists.
+ * This will archive any existing credential data and make the staged set the new one
+ * in DOMAINS.
+ * If staging is incomplete or missing, the load will fail and all credentials remain
+ * as they are.
+ *
+ * @return APR_SUCCESS on loading new data, APR_ENOENT when nothing is staged, error otherwise.
+ */
+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 */
diff --git a/modules/md/md_result.c b/modules/md/md_result.c
new file mode 100644
index 0000000..64a2f70
--- /dev/null
+++ b/modules/md/md_result.c
@@ -0,0 +1,285 @@
+/* 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 <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_date.h>
+#include <apr_time.h>
+#include <apr_strings.h>
+
+#include "md.h"
+#include "md_json.h"
+#include "md_log.h"
+#include "md_result.h"
+
+static const char *dup_trim(apr_pool_t *p, const char *s)
+{
+ char *d = apr_pstrdup(p, s);
+ if (d) apr_collapse_spaces(d, d);
+ return d;
+}
+
+md_result_t *md_result_make(apr_pool_t *p, apr_status_t status)
+{
+ md_result_t *result;
+
+ 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 char *md_name)
+{
+ md_result_t *result = md_result_make(p, APR_SUCCESS);
+ result->md_name = md_name;
+ return result;
+}
+
+void md_result_reset(md_result_t *result)
+{
+ apr_pool_t *p = result->p;
+ memset(result, 0, sizeof(*result));
+ result->p = p;
+}
+
+static void on_change(md_result_t *result)
+{
+ if (result->on_change) result->on_change(result, result->on_change_data);
+}
+
+void md_result_activity_set(md_result_t *result, const char *activity)
+{
+ md_result_activity_setn(result, activity? apr_pstrdup(result->p, activity) : NULL);
+}
+
+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);
+}
+
+void md_result_activity_printf(md_result_t *result, const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ md_result_activity_setn(result, apr_pvsprintf(result->p, fmt, ap));
+ va_end(ap);
+}
+
+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 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);
+}
+
+void md_result_problem_printf(md_result_t *result, apr_status_t status,
+ const char *problem, const char *fmt, ...)
+{
+ va_list ap;
+
+ result->status = status;
+ result->problem = dup_trim(result->p, problem);
+
+ va_start(ap, fmt);
+ result->detail = apr_pvsprintf(result->p, fmt, ap);
+ va_end(ap);
+ result->subproblems = NULL;
+ on_change(result);
+}
+
+void md_result_printf(md_result_t *result, apr_status_t status, const char *fmt, ...)
+{
+ va_list ap;
+
+ result->status = status;
+ va_start(ap, fmt);
+ result->detail = apr_pvsprintf(result->p, fmt, ap);
+ va_end(ap);
+ result->subproblems = NULL;
+ on_change(result);
+}
+
+void md_result_delay_set(md_result_t *result, apr_time_t ready_at)
+{
+ result->ready_at = ready_at;
+ on_change(result);
+}
+
+md_result_t*md_result_from_json(const struct md_json_t *json, apr_pool_t *p)
+{
+ md_result_t *result;
+ const char *s;
+
+ result = md_result_make(p, APR_SUCCESS);
+ result->status = (int)md_json_getl(json, MD_KEY_STATUS, NULL);
+ result->problem = md_json_dups(p, json, MD_KEY_PROBLEM, NULL);
+ result->detail = md_json_dups(p, json, MD_KEY_DETAIL, NULL);
+ 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;
+}
+
+struct md_json_t *md_result_to_json(const md_result_t *result, apr_pool_t *p)
+{
+ md_json_t *json;
+ char ts[APR_RFC822_DATE_LEN];
+
+ json = md_json_create(p);
+ md_json_setl(result->status, json, MD_KEY_STATUS, NULL);
+ if (result->status > 0) {
+ char buffer[HUGE_STRING_LEN];
+ apr_strerror(result->status, buffer, sizeof(buffer));
+ md_json_sets(buffer, json, "status-description", NULL);
+ }
+ if (result->problem) md_json_sets(result->problem, json, MD_KEY_PROBLEM, NULL);
+ if (result->detail) md_json_sets(result->detail, json, MD_KEY_DETAIL, NULL);
+ if (result->activity) md_json_sets(result->activity, json, MD_KEY_ACTIVITY, NULL);
+ if (result->ready_at > 0) {
+ 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;
+}
+
+static int str_cmp(const char *s1, const char *s2)
+{
+ if (s1 == s2) return 0;
+ if (!s1) return -1;
+ if (!s2) return 1;
+ return strcmp(s1, s2);
+}
+
+int md_result_cmp(const md_result_t *r1, const md_result_t *r2)
+{
+ int n;
+ if (r1 == r2) return 0;
+ if (!r1) return -1;
+ if (!r2) return 1;
+ if ((n = r1->status - r2->status)) return n;
+ if ((n = str_cmp(r1->problem, r2->problem))) return n;
+ if ((n = str_cmp(r1->detail, r2->detail))) return n;
+ if ((n = str_cmp(r1->activity, r2->activity))) return n;
+ return (int)(r1->ready_at - r2->ready_at);
+}
+
+void md_result_assign(md_result_t *dest, const md_result_t *src)
+{
+ dest->status = src->status;
+ dest->problem = src->problem;
+ 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)
+{
+ dest->status = src->status;
+ dest->problem = src->problem? dup_trim(dest->p, src->problem) : NULL;
+ 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, unsigned int level)
+{
+ if (md_log_is_level(result->p, (md_log_level_t)level)) {
+ const char *sep = "";
+ const char *msg = "";
+
+ if (result->md_name) {
+ msg = apr_psprintf(result->p, "md[%s]", result->md_name);
+ sep = " ";
+ }
+ if (result->activity) {
+ msg = apr_psprintf(result->p, "%s%swhile[%s]", msg, sep, result->activity);
+ sep = " ";
+ }
+ if (result->problem) {
+ msg = apr_psprintf(result->p, "%s%sproblem[%s]", msg, sep, result->problem);
+ sep = " ";
+ }
+ if (result->detail) {
+ 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);
+ }
+}
+
+void md_result_on_change(md_result_t *result, md_result_change_cb *cb, void *data)
+{
+ result->on_change = cb;
+ result->on_change_data = data;
+}
+
+apr_status_t md_result_raise(md_result_t *result, const char *event, apr_pool_t *p)
+{
+ if (result->on_raise) return result->on_raise(result, result->on_raise_data, event, p);
+ return APR_SUCCESS;
+}
+
+void md_result_holler(md_result_t *result, const char *event, apr_pool_t *p)
+{
+ if (result->on_holler) result->on_holler(result, result->on_holler_data, event, p);
+}
+
+void md_result_on_raise(md_result_t *result, md_result_raise_cb *cb, void *data)
+{
+ result->on_raise = cb;
+ result->on_raise_data = data;
+}
+
+void md_result_on_holler(md_result_t *result, md_result_holler_cb *cb, void *data)
+{
+ result->on_holler = cb;
+ result->on_holler_data = data;
+}
diff --git a/modules/md/md_result.h b/modules/md/md_result.h
new file mode 100644
index 0000000..e83bdd2
--- /dev/null
+++ b/modules/md/md_result.h
@@ -0,0 +1,87 @@
+/* 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_result_h
+#define mod_md_md_result_h
+
+struct md_json_t;
+struct md_t;
+
+typedef struct md_result_t md_result_t;
+
+typedef void md_result_change_cb(md_result_t *result, void *data);
+typedef apr_status_t md_result_raise_cb(md_result_t *result, void *data, const char *event, apr_pool_t *p);
+typedef void md_result_holler_cb(md_result_t *result, void *data, const char *event, apr_pool_t *p);
+
+struct md_result_t {
+ apr_pool_t *p;
+ 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;
+ void *on_change_data;
+ md_result_raise_cb *on_raise;
+ void *on_raise_data;
+ md_result_holler_cb *on_holler;
+ void *on_holler_data;
+};
+
+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 char *md_name);
+void md_result_reset(md_result_t *result);
+
+void md_result_activity_set(md_result_t *result, const char *activity);
+void md_result_activity_setn(md_result_t *result, const char *activity);
+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 struct md_json_t *subproblems);
+void md_result_problem_printf(md_result_t *result, apr_status_t status,
+ const char *problem, const char *fmt, ...);
+
+#define MD_RESULT_LOG_ID(logno) "urn:org:apache:httpd:log:"logno
+
+void md_result_printf(md_result_t *result, apr_status_t status, const char *fmt, ...);
+
+void md_result_delay_set(md_result_t *result, apr_time_t ready_at);
+
+md_result_t*md_result_from_json(const struct md_json_t *json, apr_pool_t *p);
+struct md_json_t *md_result_to_json(const md_result_t *result, apr_pool_t *p);
+
+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, unsigned int level);
+
+void md_result_on_change(md_result_t *result, md_result_change_cb *cb, void *data);
+
+/* events in the context of a result genesis */
+
+apr_status_t md_result_raise(md_result_t *result, const char *event, apr_pool_t *p);
+void md_result_holler(md_result_t *result, const char *event, apr_pool_t *p);
+
+void md_result_on_raise(md_result_t *result, md_result_raise_cb *cb, void *data);
+void md_result_on_holler(md_result_t *result, md_result_holler_cb *cb, void *data);
+
+#endif /* mod_md_md_result_h */
diff --git a/modules/md/md_status.c b/modules/md/md_status.c
new file mode 100644
index 0000000..936c653
--- /dev/null
+++ b/modules/md/md_status.c
@@ -0,0 +1,653 @@
+/* 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 <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+#include <apr_time.h>
+#include <apr_date.h>
+
+#include "md_json.h"
+#include "md.h"
+#include "md_acme.h"
+#include "md_crypt.h"
+#include "md_event.h"
+#include "md_log.h"
+#include "md_ocsp.h"
+#include "md_store.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_util.h"
+#include "md_status.h"
+
+#define MD_STATUS_WITH_SCTS 0
+
+/**************************************************************************************************/
+/* certificate status information */
+
+static apr_status_t status_get_cert_json(md_json_t **pjson, const md_cert_t *cert, apr_pool_t *p)
+{
+ const char *finger;
+ apr_status_t rv = APR_SUCCESS;
+ md_timeperiod_t valid;
+ md_json_t *json;
+
+ json = md_json_create(p);
+ 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);
+
+#if MD_STATUS_WITH_SCTS
+ do {
+ apr_array_header_t *scts;
+ const char *hex;
+ const md_sct *sct;
+ md_json_t *sctj;
+ int i;
+
+ scts = apr_array_make(p, 5, sizeof(const md_sct*));
+ if (APR_SUCCESS == md_cert_get_ct_scts(scts, p, cert)) {
+ for (i = 0; i < scts->nelts; ++i) {
+ sct = APR_ARRAY_IDX(scts, i, const md_sct*);
+ sctj = md_json_create(p);
+
+ apr_rfc822_date(ts, sct->timestamp);
+ md_json_sets(ts, sctj, "signed", NULL);
+ md_json_setl(sct->version, sctj, MD_KEY_VERSION, NULL);
+ md_data_to_hex(&hex, 0, p, sct->logid);
+ md_json_sets(hex, sctj, "logid", NULL);
+ md_data_to_hex(&hex, 0, p, sct->signature);
+ md_json_sets(hex, sctj, "signature", NULL);
+ md_json_sets(md_nid_get_sname(sct->signature_type_nid), sctj, "signature-type", NULL);
+ md_json_addj(sctj, json, "scts", NULL);
+ }
+ }
+ while (0);
+#endif
+leave:
+ *pjson = (APR_SUCCESS == rv)? json : NULL;
+ return rv;
+}
+
+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);
+ 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;
+}
+
+static apr_status_t status_get_cert_json_ex(
+ md_json_t **pjson,
+ const md_cert_t *cert,
+ const md_t *md,
+ md_reg_t *reg,
+ md_ocsp_reg_t *ocsp,
+ int with_logs,
+ apr_pool_t *p)
+{
+ md_json_t *certj, *jobj;
+ md_timeperiod_t ocsp_valid;
+ md_ocsp_cert_stat_t cert_stat;
+ apr_status_t rv;
+
+ 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);
+ }
+ }
+leave:
+ *pjson = (APR_SUCCESS == rv)? certj : NULL;
+ return rv;
+}
+
+static int get_cert_count(const md_t *md, int from_staging)
+{
+ if (!from_staging && md->cert_files && md->cert_files->nelts) {
+ return md->cert_files->nelts;
+ }
+ return md_pkeys_spec_count(md->pks);
+}
+
+static const char *get_cert_name(const md_t *md, int i, int from_staging, apr_pool_t *p)
+{
+ if (!from_staging && md->cert_files && md->cert_files->nelts) {
+ /* static files configured, not from staging, used index names */
+ return apr_psprintf(p, "%d", i);
+ }
+ return md_pkey_spec_name(md_pkeys_spec_get(md->pks, i));
+}
+
+static apr_status_t status_get_certs_json(md_json_t **pjson, apr_array_header_t *certs,
+ int from_staging,
+ const md_t *md, md_reg_t *reg,
+ md_ocsp_reg_t *ocsp, int with_logs,
+ apr_pool_t *p)
+{
+ md_json_t *json, *certj;
+ md_timeperiod_t certs_valid = {0, 0}, valid;
+ md_cert_t *cert;
+ int i;
+ apr_status_t rv = APR_SUCCESS;
+
+ json = md_json_create(p);
+ for (i = 0; i < get_cert_count(md, from_staging); ++i) {
+ cert = APR_ARRAY_IDX(certs, i, md_cert_t*);
+ if (!cert) continue;
+
+ rv = status_get_cert_json_ex(&certj, cert, md, reg, ocsp, with_logs, p);
+ if (APR_SUCCESS != rv) goto leave;
+ valid = md_cert_get_valid(cert);
+ certs_valid = i? md_timeperiod_common(&certs_valid, &valid) : valid;
+ md_json_setj(certj, json, get_cert_name(md, i, from_staging, p), NULL);
+ }
+
+ if (certs_valid.start) {
+ md_json_set_timeperiod(&certs_valid, json, MD_KEY_VALID, NULL);
+ }
+leave:
+ *pjson = (APR_SUCCESS == rv)? json : NULL;
+ return rv;
+}
+
+static apr_status_t get_staging_certs_json(md_json_t **pjson, const md_t *md,
+ md_reg_t *reg, apr_pool_t *p)
+{
+ md_pkey_spec_t *spec;
+ int i;
+ apr_array_header_t *chain, *certs;
+ const md_cert_t *cert;
+ apr_status_t rv;
+
+ certs = apr_array_make(p, 5, sizeof(md_cert_t*));
+ for (i = 0; i < get_cert_count(md, 1); ++i) {
+ spec = md_pkeys_spec_get(md->pks, i);
+ cert = NULL;
+ rv = md_pubcert_load(md_reg_store_get(reg), MD_SG_STAGING, md->name, spec, &chain, p);
+ if (APR_SUCCESS == rv) {
+ cert = APR_ARRAY_IDX(chain, 0, const md_cert_t*);
+ }
+ APR_ARRAY_PUSH(certs, const md_cert_t*) = cert;
+ }
+ return status_get_certs_json(pjson, certs, 1, md, reg, NULL, 0, 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, *certsj, *jobj;
+ int renew;
+ const md_pubcert_t *pubcert;
+ const md_cert_t *cert = NULL;
+ apr_array_header_t *certs;
+ apr_status_t rv = APR_SUCCESS;
+ apr_time_t renew_at;
+ int i;
+
+ mdj = md_to_public_json(md, p);
+ certs = apr_array_make(p, 5, sizeof(md_cert_t*));
+ for (i = 0; i < get_cert_count(md, 0); ++i) {
+ cert = NULL;
+ if (APR_SUCCESS == md_reg_get_pubcert(&pubcert, reg, md, i, p)) {
+ cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*);
+ }
+ APR_ARRAY_PUSH(certs, const md_cert_t*) = cert;
+ }
+
+ rv = status_get_certs_json(&certsj, certs, 0, md, reg, ocsp, with_logs, p);
+ if (APR_SUCCESS != rv) goto leave;
+ md_json_setj(certsj, mdj, MD_KEY_CERT, NULL);
+
+ renew_at = md_reg_renew_at(reg, md, p);
+ if (renew_at > 0) {
+ 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);
+ if (renew) {
+ 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) {
+ if (APR_SUCCESS == get_staging_certs_json(&certsj, md, reg, p)) {
+ md_json_setj(certsj, 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:
+ 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, md_ocsp_reg_t *ocsp, apr_pool_t *p)
+{
+ md_json_t *json, *mdj;
+ const md_t *md;
+ int i;
+
+ json = md_json_create(p);
+ 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 *);
+ status_get_md_json(&mdj, md, reg, ocsp, 0, p);
+ md_json_addj(mdj, json, MD_KEY_MDS, NULL);
+ }
+ *pjson = json;
+ return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* drive job persistence */
+
+md_job_t *md_job_make(apr_pool_t *p, md_store_t *store,
+ md_store_group_t group, const char *name,
+ apr_time_t min_delay)
+{
+ md_job_t *job = apr_pcalloc(p, sizeof(*job));
+ job->group = group;
+ job->mdomain = apr_pstrdup(p, name);
+ job->store = store;
+ job->p = p;
+ job->max_log = 128;
+ job->min_delay = min_delay;
+ 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->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);
+ job->notified_renewed = md_json_getb(json, MD_KEY_NOTIFIED_RENEWED, 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);
+ if (s && *s) job->last_run = apr_date_parse_rfc(s);
+ s = md_json_dups(p, json, MD_KEY_VALID_FROM, NULL);
+ if (s && *s) job->valid_from = apr_date_parse_rfc(s);
+ job->error_runs = (int)md_json_getl(json, MD_KEY_ERRORS, NULL);
+ if (md_json_has_key(json, MD_KEY_LAST, NULL)) {
+ job->last_result = md_result_from_json(md_json_getcj(json, MD_KEY_LAST, NULL), p);
+ }
+ job->log = md_json_getj(json, MD_KEY_LOG, NULL);
+}
+
+static void job_to_json(md_json_t *json, const md_job_t *job,
+ md_result_t *result, apr_pool_t *p)
+{
+ char ts[APR_RFC822_DATE_LEN];
+
+ 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);
+ md_json_setb(job->notified_renewed, json, MD_KEY_NOTIFIED_RENEWED, NULL);
+ if (job->next_run > 0) {
+ apr_rfc822_date(ts, job->next_run);
+ md_json_sets(ts, json, MD_KEY_NEXT_RUN, NULL);
+ }
+ if (job->last_run > 0) {
+ apr_rfc822_date(ts, job->last_run);
+ md_json_sets(ts, json, MD_KEY_LAST_RUN, NULL);
+ }
+ if (job->valid_from > 0) {
+ apr_rfc822_date(ts, job->valid_from);
+ md_json_sets(ts, json, MD_KEY_VALID_FROM, NULL);
+ }
+ md_json_setl(job->error_runs, json, MD_KEY_ERRORS, NULL);
+ if (!result) result = job->last_result;
+ if (result) {
+ md_json_setj(md_result_to_json(result, p), json, MD_KEY_LAST, NULL);
+ }
+ if (job->log) md_json_setj(job->log, json, MD_KEY_LOG, NULL);
+}
+
+apr_status_t md_job_load(md_job_t *job)
+{
+ md_json_t *jprops;
+ apr_status_t rv;
+
+ 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, job->p);
+ }
+ return rv;
+}
+
+apr_status_t md_job_save(md_job_t *job, md_result_t *result, apr_pool_t *p)
+{
+ 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(job->store, p, job->group, job->mdomain, MD_FN_JOB, jprops, 0);
+ if (APR_SUCCESS == rv) job->dirty = 0;
+ return rv;
+}
+
+void md_job_log_append(md_job_t *job, const char *type,
+ const char *status, const char *detail)
+{
+ md_json_t *entry;
+ char ts[APR_RFC822_DATE_LEN];
+
+ entry = md_json_create(job->p);
+ apr_rfc822_date(ts, apr_time_now());
+ md_json_sets(ts, entry, MD_KEY_WHEN, NULL);
+ md_json_sets(type, entry, MD_KEY_TYPE, NULL);
+ if (status) md_json_sets(status, entry, MD_KEY_STATUS, NULL);
+ 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 {
+ md_job_t *job;
+ const char *type;
+ md_json_t *entry;
+ size_t index;
+} log_find_ctx;
+
+static int find_first_log_entry(void *baton, size_t index, md_json_t *entry)
+{
+ log_find_ctx *ctx = baton;
+ const char *etype;
+
+ etype = md_json_gets(entry, MD_KEY_TYPE, NULL);
+ if (etype == ctx->type || (etype && ctx->type && !strcmp(etype, ctx->type))) {
+ ctx->entry = entry;
+ ctx->index = index;
+ return 0;
+ }
+ return 1;
+}
+
+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;
+ if (job->log) md_json_itera(find_first_log_entry, &ctx, job->log, MD_KEY_ENTRIES, NULL);
+ return ctx.entry;
+}
+
+apr_time_t md_job_log_get_time_of_latest(md_job_t *job, const char *type)
+{
+ md_json_t *entry;
+ const char *s;
+
+ entry = md_job_log_get_latest(job, type);
+ if (entry) {
+ s = md_json_gets(entry, MD_KEY_WHEN, NULL);
+ if (s) return apr_date_parse_rfc(s);
+ }
+ return 0;
+}
+
+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;
+ int i, complete, renewing, errored, ready, total;
+ md_json_t *json;
+
+ json = md_json_create(p);
+ complete = renewing = errored = ready = total = 0;
+ for (i = 0; i < mds->nelts; ++i) {
+ md = APR_ARRAY_IDX(mds, i, const md_t *);
+ ++total;
+ switch (md->state) {
+ case MD_S_COMPLETE: ++complete; /* fall through */
+ case MD_S_INCOMPLETE:
+ if (md_reg_should_renew(reg, md, p)) {
+ ++renewing;
+ 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) {
+ ++ready;
+ }
+ }
+ }
+ break;
+ default: ++errored; break;
+ }
+ }
+ md_json_setl(total, json, MD_KEY_TOTAL, NULL);
+ md_json_setl(complete, json, MD_KEY_COMPLETE, NULL);
+ md_json_setl(renewing, json, MD_KEY_RENEWING, NULL);
+ md_json_setl(errored, json, MD_KEY_ERRORED, NULL);
+ 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 apr_status_t job_result_raise(md_result_t *result, void *data, const char *event, apr_pool_t *p)
+{
+ md_job_result_ctx *ctx = data;
+ (void)p;
+ if (result == ctx->job->observing) {
+ return md_job_notify(ctx->job, event, result);
+ }
+ return APR_SUCCESS;
+}
+
+static void job_result_holler(md_result_t *result, void *data, const char *event, apr_pool_t *p)
+{
+ md_job_result_ctx *ctx = data;
+ if (result == ctx->job->observing) {
+ md_event_holler(event, ctx->job->mdomain, ctx->job, result, p);
+ }
+}
+
+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);
+ md_result_on_raise(result, job_result_raise, ctx);
+ md_result_on_holler(result, job_result_holler, 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(md_job_t *job, int err_count, const char *last_problem)
+{
+ apr_time_t delay = 0, max_delay = apr_time_from_sec(24*60*60); /* daily */
+ unsigned char c;
+
+ if (last_problem && md_acme_problem_is_input_related(last_problem)) {
+ /* If ACME server reported a problem and that problem indicates that our
+ * input values, e.g. our configuration, has something wrong, we always
+ * go to max delay as frequent retries are unlikely to resolve the situation.
+ * However, we should nevertheless retry daily, bc. it might be that there
+ * is a bug in the server. Unlikely, but... */
+ delay = max_delay;
+ }
+ else if (err_count > 0) {
+ /* back off duration, depending on the errors we encounter in a row */
+ delay = job->min_delay << (err_count - 1);
+ if (delay > max_delay) {
+ delay = max_delay;
+ }
+ }
+ if (delay > 0) {
+ /* jitter the delay by +/- 0-50%.
+ * Background: we see retries of jobs being too regular (e.g. all at midnight),
+ * possibly cumulating from many installations that restart their Apache at a
+ * fixed hour. This can contribute to an overload at the CA and a continuation
+ * of failure.
+ */
+ md_rand_bytes(&c, sizeof(c), job->p);
+ delay += apr_time_from_sec((apr_time_sec(delay) * (c - 128)) / 256);
+ }
+ 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, job->error_runs, result->problem);
+ }
+ 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)
+{
+ apr_status_t rv;
+
+ md_result_set(result, APR_SUCCESS, NULL);
+ rv = md_event_raise(reason, job->mdomain, job, result, job->p);
+ job->dirty = 1;
+ if (APR_SUCCESS == rv && APR_SUCCESS == result->status) {
+ job->notified = 1;
+ if (!strcmp("renewed", reason)) {
+ job->notified_renewed = 1;
+ }
+ }
+ else {
+ ++job->error_runs;
+ job->next_run = apr_time_now() + md_job_delay_on_errors(job, job->error_runs, result->problem);
+ }
+ return result->status;
+}
+
diff --git a/modules/md/md_status.h b/modules/md/md_status.h
new file mode 100644
index 0000000..f4d09bd
--- /dev/null
+++ b/modules/md/md_status.h
@@ -0,0 +1,126 @@
+/* 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_status_h
+#define md_status_h
+
+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, 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, struct md_ocsp_reg_t *ocsp,
+ apr_pool_t *p);
+
+/**
+ * Take stock of all MDs given for a short overview. The JSON returned
+ * will carry integers for MD_KEY_COMPLETE, MD_KEY_RENEWING,
+ * MD_KEY_ERRORED, MD_KEY_READY and MD_KEY_TOTAL.
+ */
+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 {
+ 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 */
+ int notified_renewed; /* true iff a 'renewed' notification was 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) */
+ apr_size_t max_log; /* max number of log entries, new ones replace oldest */
+ int dirty;
+ struct md_result_t *observing;
+ apr_time_t min_delay; /* smallest delay a repeated attempt should have */
+};
+
+/**
+ * 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, md_store_t *store,
+ md_store_group_t group, const char *name,
+ apr_time_t min_delay);
+
+void md_job_set_group(md_job_t *job, md_store_group_t group);
+
+/**
+ * Update the job from storage in <group>/job->mdomain.
+ */
+apr_status_t md_job_load(md_job_t *job);
+
+/**
+ * Update storage from job in <group>/job->mdomain.
+ */
+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.
+ * @param type type of log entry
+ * @param status status of entry (maybe NULL)
+ * @param detail description of what happened
+ */
+void md_job_log_append(md_job_t *job, const char *type,
+ const char *status, const char *detail);
+
+/**
+ * Retrieve the latest log entry of a certain type.
+ */
+md_json_t *md_job_log_get_latest(md_job_t *job, const char *type);
+
+/**
+ * Get the time the latest log entry of the given type happened, or 0 if
+ * none is found.
+ */
+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 and the last problem encountered,
+ * recommend a delay for the next attempt of job
+ */
+apr_time_t md_job_delay_on_errors(md_job_t *job, int err_count, const char *last_problem);
+
+apr_status_t md_job_notify(md_job_t *job, const char *reason, struct md_result_t *result);
+
+#endif /* md_status_h */
diff --git a/modules/md/md_store.c b/modules/md/md_store.c
new file mode 100644
index 0000000..59dbd67
--- /dev/null
+++ b/modules/md/md_store.c
@@ -0,0 +1,385 @@
+/* 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 <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_log.h"
+#include "md_json.h"
+#include "md_store.h"
+#include "md_util.h"
+
+/**************************************************************************************************/
+/* generic callback handling */
+
+#define ASPECT_MD "md.json"
+#define ASPECT_CERT "cert.pem"
+#define ASPECT_PKEY "key.pem"
+#define ASPECT_CHAIN "chain.pem"
+
+#define GNAME_ACCOUNTS
+#define GNAME_CHALLENGES
+#define GNAME_DOMAINS
+#define GNAME_STAGING
+#define GNAME_ARCHIVE
+
+static const char *GROUP_NAME[] = {
+ "none",
+ "accounts",
+ "challenges",
+ "domains",
+ "staging",
+ "archive",
+ "tmp",
+ "ocsp",
+ NULL
+};
+
+const char *md_store_group_name(unsigned int group)
+{
+ if (group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) {
+ return GROUP_NAME[group];
+ }
+ return "UNKNOWN";
+}
+
+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)
+{
+ return store->load(store, group, name, aspect, vtype, pdata, p);
+}
+
+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)
+{
+ return store->save(store, p, group, name, aspect, vtype, data, create);
+}
+
+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)
+{
+ return store->remove(store, group, name, aspect, p, force);
+}
+
+apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ const char *name)
+{
+ return store->purge(store, p, group, name);
+}
+
+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)
+{
+ return store->iterate(inspect, baton, store, p, group, pattern, aspect, vtype);
+}
+
+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)
+{
+ return md_store_load(store, group, name, aspect, MD_SV_JSON, (void**)pdata, p);
+}
+
+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)
+{
+ return md_store_save(store, p, group, name, aspect, MD_SV_JSON, (void*)data, create);
+}
+
+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)
+{
+ return store->move(store, p, from, to, name, archive);
+}
+
+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)
+{
+ if (store->get_fname) {
+ return store->get_fname(pfname, store, group, name, aspect, p);
+ }
+ return APR_ENOTIMPL;
+}
+
+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)
+{
+ 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 */
+
+typedef struct {
+ md_store_t *store;
+ md_store_group_t group;
+} md_group_ctx;
+
+apr_status_t md_load(md_store_t *store, md_store_group_t group,
+ const char *name, md_t **pmd, apr_pool_t *p)
+{
+ md_json_t *json;
+ apr_status_t rv;
+
+ rv = md_store_load_json(store, group, name, MD_FN_MD, pmd? &json : NULL, p);
+ if (APR_SUCCESS == rv) {
+ if (pmd) {
+ *pmd = md_from_json(json, p);
+ }
+ return APR_SUCCESS;
+ }
+ return rv;
+}
+
+static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_group_ctx *ctx = baton;
+ md_json_t *json;
+ md_t *md;
+ int create;
+
+ md = va_arg(ap, md_t *);
+ create = va_arg(ap, int);
+
+ json = md_to_json(md, ptemp);
+ assert(json);
+ assert(md->name);
+ return md_store_save_json(ctx->store, p, ctx->group, md->name, MD_FN_MD, json, create);
+}
+
+apr_status_t md_save(md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, md_t *md, int create)
+{
+ md_group_ctx ctx;
+
+ ctx.store = store;
+ ctx.group = group;
+ return md_util_pool_vdo(p_save, &ctx, p, md, create, NULL);
+}
+
+static apr_status_t p_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_group_ctx *ctx = baton;
+ const char *name;
+ int force;
+
+ (void)p;
+ name = va_arg(ap, const char *);
+ force = va_arg(ap, int);
+
+ assert(name);
+ return md_store_remove(ctx->store, ctx->group, name, MD_FN_MD, ptemp, force);
+}
+
+apr_status_t md_remove(md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const char *name, int force)
+{
+ md_group_ctx ctx;
+
+ ctx.store = store;
+ ctx.group = group;
+ return md_util_pool_vdo(p_remove, &ctx, p, name, force, NULL);
+}
+
+int md_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,
+ const char *name, apr_pool_t *p)
+{
+ return md_store_is_newer(store, group1, group2, name, MD_FN_MD, p);
+}
+
+
+typedef struct {
+ apr_pool_t *p;
+ apr_array_header_t *mds;
+} md_load_ctx;
+
+static const char *pk_filename(const char *keyname, const char *base, apr_pool_t *p)
+{
+ char *s, *t;
+ /* We also run on various filesystems with difference upper/lower preserve matching
+ * rules. Normalize the names we use, since private key specifications are basically
+ * user input. */
+ s = (keyname && apr_strnatcasecmp("rsa", keyname))?
+ apr_pstrcat(p, base, ".", keyname, ".pem", NULL)
+ : apr_pstrcat(p, base, ".pem", NULL);
+ for (t = s; *t; t++ )
+ *t = (char)apr_tolower(*t);
+ return s;
+}
+
+const char *md_pkey_filename(md_pkey_spec_t *spec, apr_pool_t *p)
+{
+ return pk_filename(md_pkey_spec_name(spec), "privkey", p);
+}
+
+const char *md_chain_filename(md_pkey_spec_t *spec, apr_pool_t *p)
+{
+ return pk_filename(md_pkey_spec_name(spec), "pubcert", p);
+}
+
+apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, const char *name,
+ md_pkey_spec_t *spec, md_pkey_t **ppkey, apr_pool_t *p)
+{
+ const char *fname = md_pkey_filename(spec, p);
+ return md_store_load(store, group, name, fname, MD_SV_PKEY, (void**)ppkey, p);
+}
+
+apr_status_t md_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name,
+ md_pkey_spec_t *spec, struct md_pkey_t *pkey, int create)
+{
+ const char *fname = md_pkey_filename(spec, p);
+ return md_store_save(store, p, group, name, fname, MD_SV_PKEY, pkey, create);
+}
+
+apr_status_t md_pubcert_load(md_store_t *store, md_store_group_t group, const char *name,
+ md_pkey_spec_t *spec, struct apr_array_header_t **ppubcert,
+ apr_pool_t *p)
+{
+ const char *fname = md_chain_filename(spec, p);
+ return md_store_load(store, group, name, fname, MD_SV_CHAIN, (void**)ppubcert, p);
+}
+
+apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const char *name,
+ md_pkey_spec_t *spec, struct apr_array_header_t *pubcert, int create)
+{
+ const char *fname = md_chain_filename(spec, p);
+ return md_store_save(store, p, group, name, fname, MD_SV_CHAIN, pubcert, create);
+}
+
+apr_status_t md_creds_load(md_store_t *store, md_store_group_t group, const char *name,
+ md_pkey_spec_t *spec, md_credentials_t **pcreds, apr_pool_t *p)
+{
+ md_credentials_t *creds = apr_pcalloc(p, sizeof(*creds));
+ apr_status_t rv;
+
+ creds->spec = spec;
+ if (APR_SUCCESS != (rv = md_pkey_load(store, group, name, spec, &creds->pkey, p))) {
+ goto leave;
+ }
+ /* chain is optional */
+ rv = md_pubcert_load(store, group, name, spec, &creds->chain, p);
+ if (APR_STATUS_IS_ENOENT(rv)) rv = APR_SUCCESS;
+leave:
+ *pcreds = (APR_SUCCESS == rv)? creds : NULL;
+ return rv;
+}
+
+apr_status_t md_creds_save(md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ const char *name, md_credentials_t *creds, int create)
+{
+ apr_status_t rv;
+
+ if (APR_SUCCESS != (rv = md_pkey_save(store, p, group, name, creds->spec, creds->pkey, create))) {
+ goto leave;
+ }
+ rv = md_pubcert_save(store, p, group, name, creds->spec, creds->chain, create);
+leave:
+ return rv;
+}
+
+typedef struct {
+ md_store_t *store;
+ md_store_group_t group;
+ const char *pattern;
+ const char *aspect;
+ md_store_md_inspect *inspect;
+ void *baton;
+} inspect_md_ctx;
+
+static int insp_md(void *baton, const char *name, const char *aspect,
+ md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
+{
+ inspect_md_ctx *ctx = baton;
+
+ if (!strcmp(MD_FN_MD, aspect) && vtype == MD_SV_JSON) {
+ md_t *md = md_from_json(value, ptemp);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting md at: %s", name);
+ return ctx->inspect(ctx->baton, ctx->store, md, ptemp);
+ }
+ return 1;
+}
+
+apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_store_t *store,
+ apr_pool_t *p, md_store_group_t group, const char *pattern)
+{
+ inspect_md_ctx ctx;
+
+ ctx.store = store;
+ ctx.group = group;
+ ctx.inspect = inspect;
+ ctx.baton = baton;
+
+ 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);
+}
diff --git a/modules/md/md_store.h b/modules/md/md_store.h
new file mode 100644
index 0000000..73c840f
--- /dev/null
+++ b/modules/md/md_store.h
@@ -0,0 +1,343 @@
+/* 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_store_h
+#define mod_md_md_store_h
+
+struct apr_array_header_t;
+struct md_cert_t;
+struct md_pkey_t;
+struct md_pkey_spec_t;
+
+const char *md_store_group_name(unsigned int group);
+
+typedef struct md_store_t md_store_t;
+
+/**
+ * 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_HTTPD_JSON "httpd.json"
+
+/* The corresponding names for current cert & key files are constructed
+ * in md_store and md_crypt.
+ */
+
+/* These three legacy filenames are only used in md_store_fs to
+ * upgrade 1.0 directories. They should not be used for any other
+ * purpose.
+ */
+#define MD_FN_PRIVKEY "privkey.pem"
+#define MD_FN_PUBCERT "pubcert.pem"
+#define MD_FN_CERT "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);
+
+/**
+ * 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 */
+
+apr_status_t md_load(md_store_t *store, md_store_group_t group,
+ const char *name, md_t **pmd, apr_pool_t *p);
+apr_status_t md_save(struct md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ md_t *md, int create);
+apr_status_t md_remove(md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ const char *name, int force);
+
+int md_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,
+ const char *name, apr_pool_t *p);
+
+typedef int md_store_md_inspect(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp);
+
+apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_store_t *store,
+ apr_pool_t *p, md_store_group_t group, const char *pattern);
+
+
+const char *md_pkey_filename(struct md_pkey_spec_t *spec, apr_pool_t *p);
+const char *md_chain_filename(struct md_pkey_spec_t *spec, apr_pool_t *p);
+
+apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group,
+ const char *name, struct md_pkey_spec_t *spec,
+ struct md_pkey_t **ppkey, apr_pool_t *p);
+apr_status_t md_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ const char *name, struct md_pkey_spec_t *spec,
+ struct md_pkey_t *pkey, int create);
+
+apr_status_t md_pubcert_load(md_store_t *store, md_store_group_t group, const char *name,
+ struct md_pkey_spec_t *spec, struct apr_array_header_t **ppubcert,
+ apr_pool_t *p);
+apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const char *name,
+ struct md_pkey_spec_t *spec,
+ struct apr_array_header_t *pubcert, int create);
+
+/**************************************************************************************************/
+/* X509 complete credentials */
+
+typedef struct md_credentials_t md_credentials_t;
+struct md_credentials_t {
+ struct md_pkey_spec_t *spec;
+ struct md_pkey_t *pkey;
+ struct apr_array_header_t *chain;
+};
+
+apr_status_t md_creds_load(md_store_t *store, md_store_group_t group, const char *name,
+ struct md_pkey_spec_t *spec, md_credentials_t **pcreds, apr_pool_t *p);
+apr_status_t md_creds_save(md_store_t *store, apr_pool_t *p, md_store_group_t group,
+ const char *name, md_credentials_t *creds, 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);
+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;
+ 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;
+ md_store_lock_global_cb *lock_global;
+ md_store_unlock_global_cb *unlock_global;
+};
+
+
+#endif /* mod_md_md_store_h */
diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c
new file mode 100644
index 0000000..35c24b4
--- /dev/null
+++ b/modules/md/md_store_fs.c
@@ -0,0 +1,1169 @@
+/* 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 <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_store_fs.h"
+#include "md_util.h"
+#include "md_version.h"
+
+/**************************************************************************************************/
+/* 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;
+ apr_fileperms_t file;
+} perms_t;
+
+typedef struct md_store_fs_t md_store_fs_t;
+struct md_store_fs_t {
+ md_store_t s;
+
+ const char *base; /* base directory of store */
+ perms_t def_perms;
+ perms_t group_perms[MD_SG_COUNT];
+ md_store_fs_cb *event_cb;
+ void *event_baton;
+
+ md_data_t key;
+ int plain_pkey[MD_SG_COUNT];
+
+ 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))
+#define FS_STORE_JSON "md_store.json"
+#define FS_STORE_KLEN 48
+
+static apr_status_t fs_load(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);
+static apr_status_t fs_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 *value, int create);
+static apr_status_t fs_remove(md_store_t *store, md_store_group_t group,
+ const char *name, const char *aspect,
+ 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);
+static apr_status_t fs_iterate_names(md_store_inspect *inspect, void *baton, md_store_t *store,
+ apr_pool_t *p, md_store_group_t group, const char *pattern);
+
+static apr_status_t fs_get_fname(const char **pfname,
+ md_store_t *store, md_store_group_t group,
+ const char *name, const char *aspect,
+ apr_pool_t *p);
+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 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)
+{
+ md_json_t *json = md_json_create(p);
+ const char *key64;
+ apr_status_t rv;
+
+ md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL);
+
+ md_data_pinit(&s_fs->key, FS_STORE_KLEN, p);
+ 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(&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));
+
+ return rv;
+}
+
+static apr_status_t rename_pkey(void *baton, apr_pool_t *p, apr_pool_t *ptemp,
+ const char *dir, const char *name,
+ apr_filetype_e ftype)
+{
+ const char *from, *to;
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)baton;
+ (void)ftype;
+ if ( MD_OK(md_util_path_merge(&from, ptemp, dir, name, NULL))
+ && MD_OK(md_util_path_merge(&to, ptemp, dir, MD_FN_PRIVKEY, NULL))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "renaming %s/%s to %s",
+ dir, name, MD_FN_PRIVKEY);
+ return apr_file_rename(from, to, ptemp);
+ }
+ return rv;
+}
+
+static apr_status_t mk_pubcert(void *baton, apr_pool_t *p, apr_pool_t *ptemp,
+ const char *dir, const char *name,
+ apr_filetype_e ftype)
+{
+ md_cert_t *cert;
+ apr_array_header_t *chain, *pubcert;
+ const char *fname, *fpubcert;
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)baton;
+ (void)ftype;
+ (void)p;
+ if ( MD_OK(md_util_path_merge(&fpubcert, ptemp, dir, MD_FN_PUBCERT, NULL))
+ && APR_STATUS_IS_ENOENT(rv = md_chain_fload(&pubcert, ptemp, fpubcert))
+ && MD_OK(md_util_path_merge(&fname, ptemp, dir, name, NULL))
+ && MD_OK(md_cert_fload(&cert, ptemp, fname))
+ && MD_OK(md_util_path_merge(&fname, ptemp, dir, "chain.pem", NULL))) {
+
+ rv = md_chain_fload(&chain, ptemp, fname);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ chain = apr_array_make(ptemp, 1, sizeof(md_cert_t*));
+ rv = APR_SUCCESS;
+ }
+ if (APR_SUCCESS == rv) {
+ pubcert = apr_array_make(ptemp, chain->nelts + 1, sizeof(md_cert_t*));
+ APR_ARRAY_PUSH(pubcert, md_cert_t *) = cert;
+ apr_array_cat(pubcert, chain);
+ rv = md_chain_fsave(pubcert, ptemp, fpubcert, MD_FPROT_F_UONLY);
+ }
+ }
+ return rv;
+}
+
+static apr_status_t upgrade_from_1_0(md_store_fs_t *s_fs, apr_pool_t *p, apr_pool_t *ptemp)
+{
+ md_store_group_t g;
+ apr_status_t rv = APR_SUCCESS;
+
+ (void)ptemp;
+ /* Migrate pkey.pem -> privkey.pem */
+ for (g = MD_SG_NONE; g < MD_SG_COUNT && APR_SUCCESS == rv; ++g) {
+ rv = md_util_files_do(rename_pkey, s_fs, p, s_fs->base,
+ md_store_group_name(g), "*", "pkey.pem", NULL);
+ }
+ /* Generate fullcert.pem from cert.pem and chain.pem where missing */
+ rv = md_util_files_do(mk_pubcert, s_fs, p, s_fs->base,
+ md_store_group_name(MD_SG_DOMAINS), "*", MD_FN_CERT, NULL);
+ rv = md_util_files_do(mk_pubcert, s_fs, p, s_fs->base,
+ md_store_group_name(MD_SG_ARCHIVE), "*", MD_FN_CERT, NULL);
+
+ return rv;
+}
+
+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;
+ apr_status_t rv;
+ double store_version;
+
+ if (MD_OK(md_json_readf(&json, p, fname))) {
+ store_version = md_json_getn(json, MD_KEY_STORE, MD_KEY_VERSION, NULL);
+ if (store_version <= 0.0) {
+ /* ok, an old one, compatible to 1.0 */
+ store_version = 1.0;
+ }
+ if (store_version > MD_STORE_VERSION) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "version too new: %f", store_version);
+ return APR_EINVAL;
+ }
+
+ key64 = md_json_dups(p, json, MD_KEY_KEY, NULL);
+ if (!key64) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "missing key: %s", MD_KEY_KEY);
+ return APR_EINVAL;
+ }
+
+ 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);
+ return APR_EINVAL;
+ }
+
+ /* Need to migrate format? */
+ if (store_version < MD_STORE_VERSION) {
+ if (store_version <= 1.0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "migrating store v1 -> v2");
+ rv = upgrade_from_1_0(s_fs, p, ptemp);
+ }
+ if (store_version <= 2.0) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "migrating store v2 -> v3");
+ md_json_del(json, MD_KEY_VERSION, NULL);
+ }
+
+ if (APR_SUCCESS == rv) {
+ md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL);
+ rv = md_json_freplace(json, ptemp, MD_JSON_FMT_INDENT, fname, MD_FPROT_F_UONLY);
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, "migrated store");
+ }
+ }
+ return rv;
+}
+
+static apr_status_t setup_store_file(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_fs_t *s_fs = baton;
+ const char *fname;
+ apr_status_t rv;
+
+ (void)ap;
+ s_fs->plain_pkey[MD_SG_DOMAINS] = 1;
+ /* Added: the encryption of tls-alpn-01 certificate keys is not a security issue
+ * for these self-signed, short-lived certificates. Having them unencrypted let's
+ * use pass around the files insteak of an *SSL implementation dependent PKEY_something.
+ */
+ s_fs->plain_pkey[MD_SG_CHALLENGES] = 1;
+ s_fs->plain_pkey[MD_SG_TMP] = 1;
+
+ if (!MD_OK(md_util_path_merge(&fname, ptemp, s_fs->base, FS_STORE_JSON, NULL))) {
+ return rv;
+ }
+
+read:
+ if (MD_OK(md_util_is_file(fname, ptemp))) {
+ rv = read_store_file(s_fs, fname, p, ptemp);
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)
+ && APR_STATUS_IS_EEXIST(rv = init_store_file(s_fs, fname, p, ptemp))) {
+ goto read;
+ }
+ return rv;
+}
+
+apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *path)
+{
+ md_store_fs_t *s_fs;
+ apr_status_t rv = APR_SUCCESS;
+
+ s_fs = apr_pcalloc(p, sizeof(*s_fs));
+
+ s_fs->s.load = fs_load;
+ 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;
+ 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;
+
+ /* Account information needs to be accessible to httpd child processes.
+ * private keys are, similar to staging, encrypted. */
+ s_fs->group_perms[MD_SG_ACCOUNTS].dir = MD_FPROT_D_UALL_WREAD;
+ s_fs->group_perms[MD_SG_ACCOUNTS].file = MD_FPROT_F_UALL_WREAD;
+ s_fs->group_perms[MD_SG_STAGING].dir = MD_FPROT_D_UALL_WREAD;
+ s_fs->group_perms[MD_SG_STAGING].file = MD_FPROT_F_UALL_WREAD;
+ /* 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);
+
+ rv = md_util_is_dir(s_fs->base, p);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p,
+ "store directory does not exist, creating %s", s_fs->base);
+ rv = apr_dir_make_recursive(s_fs->base, s_fs->def_perms.dir, p);
+ if (APR_SUCCESS != rv) goto cleanup;
+ rv = apr_file_perms_set(s_fs->base, MD_FPROT_D_UALL_WREAD);
+ if (APR_STATUS_IS_ENOTIMPL(rv)) {
+ rv = APR_SUCCESS;
+ }
+ if (APR_SUCCESS != rv) goto cleanup;
+ }
+ else if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+ "not a plain directory, maybe a symlink? %s", s_fs->base);
+ }
+
+ rv = md_util_pool_vdo(setup_store_file, s_fs, p, NULL);
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "init fs store at %s", s_fs->base);
+ }
+cleanup:
+ *pstore = (rv == APR_SUCCESS)? &(s_fs->s) : NULL;
+ return rv;
+}
+
+apr_status_t md_store_fs_default_perms_set(md_store_t *store,
+ apr_fileperms_t file_perms,
+ apr_fileperms_t dir_perms)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+
+ s_fs->def_perms.file = file_perms;
+ s_fs->def_perms.dir = dir_perms;
+ return APR_SUCCESS;
+}
+
+apr_status_t md_store_fs_group_perms_set(md_store_t *store, md_store_group_t group,
+ apr_fileperms_t file_perms,
+ apr_fileperms_t dir_perms)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+
+ if (group >= (sizeof(s_fs->group_perms)/sizeof(s_fs->group_perms[0]))) {
+ return APR_ENOTIMPL;
+ }
+ s_fs->group_perms[group].file = file_perms;
+ s_fs->group_perms[group].dir = dir_perms;
+ return APR_SUCCESS;
+}
+
+apr_status_t md_store_fs_set_event_cb(struct md_store_t *store, md_store_fs_cb *cb, void *baton)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+
+ s_fs->event_cb = cb;
+ s_fs->event_baton = baton;
+ return APR_SUCCESS;
+}
+
+static const perms_t *gperms(md_store_fs_t *s_fs, md_store_group_t group)
+{
+ if (group >= (sizeof(s_fs->group_perms)/sizeof(s_fs->group_perms[0]))
+ || !s_fs->group_perms[group].dir) {
+ return &s_fs->def_perms;
+ }
+ return &s_fs->group_perms[group];
+}
+
+static apr_status_t fs_get_fname(const char **pfname,
+ 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);
+ if (group == MD_SG_NONE) {
+ return md_util_path_merge(pfname, p, s_fs->base, aspect, NULL);
+ }
+ return md_util_path_merge(pfname, p,
+ s_fs->base, md_store_group_name(group), name, aspect, NULL);
+}
+
+static apr_status_t fs_get_dname(const char **pdname,
+ md_store_t *store, md_store_group_t group,
+ const char *name, apr_pool_t *p)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+ if (group == MD_SG_NONE) {
+ *pdname = s_fs->base;
+ return APR_SUCCESS;
+ }
+ return md_util_path_merge(pdname, p, s_fs->base, md_store_group_name(group), name, NULL);
+}
+
+static void get_pass(const char **ppass, apr_size_t *plen,
+ md_store_fs_t *s_fs, md_store_group_t group)
+{
+ if (s_fs->plain_pkey[group]) {
+ *ppass = NULL;
+ *plen = 0;
+ }
+ else {
+ *ppass = (const char *)s_fs->key.data;
+ *plen = s_fs->key.len;
+ }
+}
+
+static apr_status_t fs_fload(void **pvalue, md_store_fs_t *s_fs, const char *fpath,
+ md_store_group_t group, md_store_vtype_t vtype,
+ apr_pool_t *p, apr_pool_t *ptemp)
+{
+ apr_status_t rv;
+ const char *pass;
+ apr_size_t pass_len;
+
+ if (pvalue != NULL) {
+ switch (vtype) {
+ case MD_SV_TEXT:
+ rv = md_text_fread8k((const char **)pvalue, p, fpath);
+ break;
+ case MD_SV_JSON:
+ rv = md_json_readf((md_json_t **)pvalue, p, fpath);
+ break;
+ case MD_SV_CERT:
+ rv = md_cert_fload((md_cert_t **)pvalue, p, fpath);
+ break;
+ case MD_SV_PKEY:
+ get_pass(&pass, &pass_len, s_fs, group);
+ rv = md_pkey_fload((md_pkey_t **)pvalue, p, pass, pass_len, fpath);
+ break;
+ case MD_SV_CHAIN:
+ rv = md_chain_fload((apr_array_header_t **)pvalue, p, fpath);
+ break;
+ default:
+ rv = APR_ENOTIMPL;
+ break;
+ }
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp,
+ "loading type %d from %s", vtype, fpath);
+ }
+ else { /* check for existence only */
+ rv = md_util_is_file(fpath, p);
+ }
+ return rv;
+}
+
+static apr_status_t pfs_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_fs_t *s_fs = baton;
+ const char *fpath, *name, *aspect;
+ md_store_vtype_t vtype;
+ md_store_group_t group;
+ void **pvalue;
+ apr_status_t rv;
+
+ group = (md_store_group_t)va_arg(ap, int);
+ name = va_arg(ap, const char *);
+ aspect = va_arg(ap, const char *);
+ vtype = (md_store_vtype_t)va_arg(ap, int);
+ pvalue= va_arg(ap, void **);
+
+ if (MD_OK(fs_get_fname(&fpath, &s_fs->s, group, name, aspect, ptemp))) {
+ rv = fs_fload(pvalue, s_fs, fpath, group, vtype, p, ptemp);
+ }
+ return rv;
+}
+
+static apr_status_t dispatch(md_store_fs_t *s_fs, md_store_fs_ev_t ev, unsigned int group,
+ const char *fname, apr_filetype_e ftype, apr_pool_t *p)
+{
+ (void)ev;
+ if (s_fs->event_cb) {
+ return s_fs->event_cb(s_fs->event_baton, &s_fs->s, MD_S_FS_EV_CREATED,
+ group, fname, ftype, p);
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs,
+ md_store_group_t group, const char *name,
+ apr_pool_t *p)
+{
+ const perms_t *perms;
+ apr_status_t rv;
+
+ perms = gperms(s_fs, group);
+
+ *pdir = NULL;
+ rv = fs_get_dname(pdir, &s_fs->s, group, name, p);
+ if ((APR_SUCCESS != rv) || (MD_SG_NONE == group)) goto cleanup;
+
+ rv = md_util_is_dir(*pdir, p);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "not a directory, creating %s", *pdir);
+ rv = apr_dir_make_recursive(*pdir, perms->dir, p);
+ if (APR_SUCCESS != rv) goto cleanup;
+ dispatch(s_fs, MD_S_FS_EV_CREATED, group, *pdir, APR_DIR, p);
+ }
+
+ rv = apr_file_perms_set(*pdir, perms->dir);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "mk_group_dir %s perm set", *pdir);
+ if (APR_STATUS_IS_ENOTIMPL(rv)) {
+ rv = APR_SUCCESS;
+ }
+cleanup:
+ if (APR_SUCCESS != rv) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "mk_group_dir %d %s",
+ group, (*pdir? *pdir : (name? name : "(null)")));
+ }
+ return rv;
+}
+
+static apr_status_t pfs_is_newer(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_fs_t *s_fs = baton;
+ const char *fname1, *fname2, *name, *aspect;
+ md_store_group_t group1, group2;
+ apr_finfo_t inf1, inf2;
+ int *pnewer;
+ apr_status_t rv;
+
+ (void)p;
+ group1 = (md_store_group_t)va_arg(ap, int);
+ group2 = (md_store_group_t)va_arg(ap, int);
+ name = va_arg(ap, const char*);
+ aspect = va_arg(ap, const char*);
+ pnewer = va_arg(ap, int*);
+
+ *pnewer = 0;
+ if ( MD_OK(fs_get_fname(&fname1, &s_fs->s, group1, name, aspect, ptemp))
+ && MD_OK(fs_get_fname(&fname2, &s_fs->s, group2, name, aspect, ptemp))
+ && MD_OK(apr_stat(&inf1, fname1, APR_FINFO_MTIME, ptemp))
+ && MD_OK(apr_stat(&inf2, fname2, APR_FINFO_MTIME, ptemp))) {
+ *pnewer = inf1.mtime > inf2.mtime;
+ }
+
+ 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)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+ int newer = 0;
+ apr_status_t rv;
+
+ rv = md_util_pool_vdo(pfs_is_newer, s_fs, p, group1, group2, name, aspect, &newer, NULL);
+ if (APR_SUCCESS == rv) {
+ return newer;
+ }
+ 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;
+ const char *gdir, *dir, *fpath, *name, *aspect;
+ md_store_vtype_t vtype;
+ md_store_group_t group;
+ void *value;
+ int create;
+ apr_status_t rv;
+ const perms_t *perms;
+ const char *pass;
+ apr_size_t pass_len;
+
+ group = (md_store_group_t)va_arg(ap, int);
+ name = va_arg(ap, const char*);
+ aspect = va_arg(ap, const char*);
+ vtype = (md_store_vtype_t)va_arg(ap, int);
+ value = va_arg(ap, void *);
+ create = va_arg(ap, int);
+
+ perms = gperms(s_fs, group);
+
+ if ( MD_OK(mk_group_dir(&gdir, s_fs, group, NULL, p))
+ && MD_OK(mk_group_dir(&dir, s_fs, group, name, p))
+ && MD_OK(md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) {
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, ptemp, "storing in %s", fpath);
+ switch (vtype) {
+ case MD_SV_TEXT:
+ rv = (create? md_text_fcreatex(fpath, perms->file, p, value)
+ : md_text_freplace(fpath, perms->file, p, value));
+ break;
+ case MD_SV_JSON:
+ rv = (create? md_json_fcreatex((md_json_t *)value, p, MD_JSON_FMT_INDENT,
+ fpath, perms->file)
+ : md_json_freplace((md_json_t *)value, p, MD_JSON_FMT_INDENT,
+ fpath, perms->file));
+ break;
+ case MD_SV_CERT:
+ rv = md_cert_fsave((md_cert_t *)value, ptemp, fpath, perms->file);
+ break;
+ case MD_SV_PKEY:
+ /* Take care that we write private key with access only to the user,
+ * unless we write the key encrypted */
+ get_pass(&pass, &pass_len, s_fs, group);
+ rv = md_pkey_fsave((md_pkey_t *)value, ptemp, pass, pass_len,
+ fpath, (pass && pass_len)? perms->file : MD_FPROT_F_UONLY);
+ break;
+ case MD_SV_CHAIN:
+ rv = md_chain_fsave((apr_array_header_t*)value, ptemp, fpath, perms->file);
+ break;
+ default:
+ return APR_ENOTIMPL;
+ }
+ if (APR_SUCCESS == rv) {
+ rv = dispatch(s_fs, MD_S_FS_EV_CREATED, group, fpath, APR_REG, p);
+ }
+ }
+ return rv;
+}
+
+static apr_status_t pfs_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_fs_t *s_fs = baton;
+ const char *dir, *name, *fpath, *groupname, *aspect;
+ apr_status_t rv;
+ int force;
+ apr_finfo_t info;
+ md_store_group_t group;
+
+ (void)p;
+ group = (md_store_group_t)va_arg(ap, int);
+ name = va_arg(ap, const char*);
+ aspect = va_arg(ap, const char *);
+ force = va_arg(ap, int);
+
+ groupname = md_store_group_name(group);
+
+ if ( MD_OK(md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL))
+ && MD_OK(md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "start remove of md %s/%s/%s",
+ groupname, name, aspect);
+
+ if (!MD_OK(apr_stat(&info, dir, APR_FINFO_TYPE, ptemp))) {
+ if (APR_ENOENT == rv && force) {
+ return APR_SUCCESS;
+ }
+ return rv;
+ }
+
+ rv = apr_file_remove(fpath, ptemp);
+ if (APR_ENOENT == rv && force) {
+ rv = APR_SUCCESS;
+ }
+ }
+ return rv;
+}
+
+static apr_status_t fs_load(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)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+ return md_util_pool_vdo(pfs_load, s_fs, p, group, name, aspect, vtype, pvalue, NULL);
+}
+
+static apr_status_t fs_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 *value, int create)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+ return md_util_pool_vdo(pfs_save, s_fs, p, group, name, aspect,
+ vtype, value, create, NULL);
+}
+
+static apr_status_t fs_remove(md_store_t *store, md_store_group_t group,
+ const char *name, const char *aspect,
+ apr_pool_t *p, int force)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+ return md_util_pool_vdo(pfs_remove, s_fs, p, group, name, aspect, force, NULL);
+}
+
+static apr_status_t pfs_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_fs_t *s_fs = baton;
+ const char *dir, *name, *groupname;
+ md_store_group_t group;
+ apr_status_t rv;
+
+ (void)p;
+ group = (md_store_group_t)va_arg(ap, int);
+ name = va_arg(ap, const char*);
+
+ groupname = md_store_group_name(group);
+
+ if (MD_OK(md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL))) {
+ /* Remove all files in dir, there should be no sub-dirs */
+ rv = md_util_rm_recursive(dir, ptemp, 1);
+ }
+ if (!APR_STATUS_IS_ENOENT(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, ptemp, "purge %s/%s (%s)", groupname, name, dir);
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t fs_purge(md_store_t *store, apr_pool_t *p,
+ md_store_group_t group, const char *name)
+{
+ md_store_fs_t *s_fs = FS_STORE(store);
+ return md_util_pool_vdo(pfs_purge, s_fs, p, group, name, NULL);
+}
+
+/**************************************************************************************************/
+/* iteration */
+
+typedef struct {
+ md_store_fs_t *s_fs;
+ md_store_group_t group;
+ const char *pattern;
+ const char *aspect;
+ md_store_vtype_t vtype;
+ 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,
+ const char *dir, const char *name, apr_filetype_e ftype)
+{
+ inspect_ctx *ctx = baton;
+ apr_status_t rv;
+ void *value;
+ const char *fpath;
+
+ (void)ftype;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting value at: %s/%s", dir, name);
+ if (APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, name, NULL))) {
+ rv = fs_fload(&value, ctx->s_fs, fpath, ctx->group, ctx->vtype, p, ptemp);
+ if (APR_SUCCESS == rv
+ && !ctx->inspect(ctx->baton, ctx->dirname, name, ctx->vtype, value, p)) {
+ return APR_EOF;
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ }
+ }
+ return rv;
+}
+
+static apr_status_t insp_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, "inspecting 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(insp, ctx, p, fpath, ctx->aspect, NULL);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ }
+ }
+ return rv;
+}
+
+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)
+{
+ const char *groupname;
+ apr_status_t rv;
+ inspect_ctx ctx;
+
+ ctx.s_fs = FS_STORE(store);
+ ctx.group = group;
+ ctx.pattern = pattern;
+ ctx.aspect = aspect;
+ ctx.vtype = vtype;
+ ctx.inspect = inspect;
+ ctx.baton = baton;
+ groupname = md_store_group_name(group);
+
+ rv = md_util_files_do(insp_dir, &ctx, p, ctx.s_fs->base, groupname, pattern, NULL);
+
+ return rv;
+}
+
+static apr_status_t insp_name(void *baton, apr_pool_t *p, apr_pool_t *ptemp,
+ const char *dir, const char *name, apr_filetype_e ftype)
+{
+ inspect_ctx *ctx = baton;
+
+ (void)ftype;
+ (void)p;
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting name at: %s/%s", dir, name);
+ return ctx->inspect(ctx->baton, dir, name, 0, NULL, ptemp);
+}
+
+static apr_status_t fs_iterate_names(md_store_inspect *inspect, void *baton, md_store_t *store,
+ apr_pool_t *p, md_store_group_t group, const char *pattern)
+{
+ const char *groupname;
+ apr_status_t rv;
+ inspect_ctx ctx;
+
+ ctx.s_fs = FS_STORE(store);
+ ctx.group = group;
+ ctx.pattern = pattern;
+ ctx.inspect = inspect;
+ ctx.baton = baton;
+ groupname = md_store_group_name(group);
+
+ rv = md_util_files_do(insp_name, &ctx, p, ctx.s_fs->base, groupname, pattern, NULL);
+
+ 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 */
+
+static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_store_fs_t *s_fs = baton;
+ const char *name, *from_group, *to_group, *from_dir, *to_dir, *arch_dir, *dir;
+ md_store_group_t from, to;
+ int archive;
+ apr_status_t rv;
+
+ (void)p;
+ from = (md_store_group_t)va_arg(ap, int);
+ to = (md_store_group_t)va_arg(ap, int);
+ name = va_arg(ap, const char*);
+ archive = va_arg(ap, int);
+
+ from_group = md_store_group_name(from);
+ to_group = md_store_group_name(to);
+ if (!strcmp(from_group, to_group)) {
+ return APR_EINVAL;
+ }
+
+ if ( !MD_OK(md_util_path_merge(&from_dir, ptemp, s_fs->base, from_group, name, NULL))
+ || !MD_OK(md_util_path_merge(&to_dir, ptemp, s_fs->base, to_group, name, NULL))) {
+ goto out;
+ }
+
+ if (!MD_OK(md_util_is_dir(from_dir, ptemp))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "source is no dir: %s", from_dir);
+ goto out;
+ }
+
+ if (MD_OK(archive? md_util_is_dir(to_dir, ptemp) : APR_ENOENT)) {
+ int n = 1;
+ const char *narch_dir;
+
+ if ( !MD_OK(md_util_path_merge(&dir, ptemp, s_fs->base,
+ md_store_group_name(MD_SG_ARCHIVE), NULL))
+ || !MD_OK(apr_dir_make_recursive(dir, MD_FPROT_D_UONLY, ptemp))
+ || !MD_OK(md_util_path_merge(&arch_dir, ptemp, dir, name, NULL))) {
+ goto out;
+ }
+
+#ifdef WIN32
+ /* WIN32 and handling of files/dirs. What can one say? */
+
+ while (n < 1000) {
+ narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n);
+ rv = md_util_is_dir(narch_dir, ptemp);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "using archive dir: %s",
+ narch_dir);
+ break;
+ }
+ else {
+ ++n;
+ narch_dir = NULL;
+ }
+ }
+
+#else /* ifdef WIN32 */
+
+ while (n < 1000) {
+ narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n);
+ if (MD_OK(apr_dir_make(narch_dir, MD_FPROT_D_UONLY, ptemp))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "using archive dir: %s",
+ narch_dir);
+ break;
+ }
+ else if (APR_EEXIST == rv) {
+ ++n;
+ narch_dir = NULL;
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "creating archive dir: %s",
+ narch_dir);
+ goto out;
+ }
+ }
+
+#endif /* ifdef WIN32 (else part) */
+
+ if (!narch_dir) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "ran out of numbers less than 1000 "
+ "while looking for an available one in %s to archive the data "
+ "from %s. Either something is generally wrong or you need to "
+ "clean up some of those directories.", arch_dir, from_dir);
+ rv = APR_EGENERAL;
+ goto out;
+ }
+
+ if (!MD_OK(apr_file_rename(to_dir, narch_dir, ptemp))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s",
+ to_dir, narch_dir);
+ goto out;
+ }
+ if (!MD_OK(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);
+ apr_file_rename(narch_dir, to_dir, ptemp);
+ goto out;
+ }
+ if (MD_OK(dispatch(s_fs, MD_S_FS_EV_MOVED, to, to_dir, APR_DIR, ptemp))) {
+ rv = dispatch(s_fs, MD_S_FS_EV_MOVED, MD_SG_ARCHIVE, narch_dir, APR_DIR, ptemp);
+ }
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ 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;
+ }
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "target is no dir: %s", to_dir);
+ goto out;
+ }
+
+out:
+ return rv;
+}
+
+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)
+{
+ 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))
+ && !APR_STATUS_IS_ENOENT(rv)) {
+ 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);
+}
+
+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;
+ }
+}
diff --git a/modules/md/md_store_fs.h b/modules/md/md_store_fs.h
new file mode 100644
index 0000000..dcdb897
--- /dev/null
+++ b/modules/md/md_store_fs.h
@@ -0,0 +1,65 @@
+/* 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_store_fs_h
+#define mod_md_md_store_fs_h
+
+struct md_store_t;
+
+/**
+ * Default file permissions set by the store, user only read/write(/exec),
+ * if so supported by the apr.
+ */
+#define MD_FPROT_F_UONLY (APR_FPROT_UREAD|APR_FPROT_UWRITE)
+#define MD_FPROT_D_UONLY (MD_FPROT_F_UONLY|APR_FPROT_UEXECUTE)
+
+/**
+ * User has all permission, group can read, other none
+ */
+#define MD_FPROT_F_UALL_GREAD (MD_FPROT_F_UONLY|APR_FPROT_GREAD)
+#define MD_FPROT_D_UALL_GREAD (MD_FPROT_D_UONLY|APR_FPROT_GREAD|APR_FPROT_GEXECUTE)
+
+/**
+ * User has all permission, group and others can read
+ */
+#define MD_FPROT_F_UALL_WREAD (MD_FPROT_F_UALL_GREAD|APR_FPROT_WREAD)
+#define MD_FPROT_D_UALL_WREAD (MD_FPROT_D_UALL_GREAD|APR_FPROT_WREAD|APR_FPROT_WEXECUTE)
+
+apr_status_t md_store_fs_init(struct md_store_t **pstore, apr_pool_t *p,
+ const char *path);
+
+
+apr_status_t md_store_fs_default_perms_set(struct md_store_t *store,
+ apr_fileperms_t file_perms,
+ apr_fileperms_t dir_perms);
+apr_status_t md_store_fs_group_perms_set(struct md_store_t *store,
+ md_store_group_t group,
+ apr_fileperms_t file_perms,
+ apr_fileperms_t dir_perms);
+
+typedef enum {
+ MD_S_FS_EV_CREATED,
+ MD_S_FS_EV_MOVED,
+} md_store_fs_ev_t;
+
+typedef apr_status_t md_store_fs_cb(void *baton, struct md_store_t *store,
+ md_store_fs_ev_t ev, unsigned int group,
+ const char *fname, apr_filetype_e ftype,
+ apr_pool_t *p);
+
+apr_status_t md_store_fs_set_event_cb(struct md_store_t *store, md_store_fs_cb *cb, void *baton);
+
+#endif /* mod_md_md_store_fs_h */
diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c
new file mode 100644
index 0000000..c8d2bad
--- /dev/null
+++ b/modules/md/md_tailscale.c
@@ -0,0 +1,383 @@
+/* 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 <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_tailscale.h"
+
+typedef struct {
+ apr_pool_t *pool;
+ md_proto_driver_t *driver;
+ const char *unix_socket_path;
+ md_t *md;
+ apr_array_header_t *chain;
+ md_pkey_t *pkey;
+} ts_ctx_t;
+
+static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result)
+{
+ ts_ctx_t *ts_ctx;
+ apr_uri_t uri;
+ const char *ca_url;
+ apr_status_t rv = APR_SUCCESS;
+
+ md_result_set(result, APR_SUCCESS, NULL);
+ ts_ctx = apr_pcalloc(d->p, sizeof(*ts_ctx));
+ ts_ctx->pool = d->p;
+ ts_ctx->driver = d;
+ ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *));
+
+ ca_url = (d->md->ca_urls && !apr_is_empty_array(d->md->ca_urls))?
+ APR_ARRAY_IDX(d->md->ca_urls, 0, const char*) : NULL;
+ if (!ca_url) {
+ ca_url = MD_TAILSCALE_DEF_URL;
+ }
+ rv = apr_uri_parse(d->p, ca_url, &uri);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "error parsing CA URL `%s`", ca_url);
+ goto leave;
+ }
+ if (uri.scheme && uri.scheme[0] && strcmp("file", uri.scheme)) {
+ rv = APR_ENOTIMPL;
+ md_result_printf(result, rv, "non `file` URLs not supported, CA URL is `%s`",
+ ca_url);
+ goto leave;
+ }
+ if (uri.hostname && uri.hostname[0] && strcmp("localhost", uri.hostname)) {
+ rv = APR_ENOTIMPL;
+ md_result_printf(result, rv, "non `localhost` URLs not supported, CA URL is `%s`",
+ ca_url);
+ goto leave;
+ }
+ ts_ctx->unix_socket_path = uri.path;
+ d->baton = ts_ctx;
+
+leave:
+ return rv;
+}
+
+static apr_status_t ts_preload_init(md_proto_driver_t *d, md_result_t *result)
+{
+ return ts_init(d, result);
+}
+
+static apr_status_t ts_preload(md_proto_driver_t *d,
+ md_store_group_t load_group, md_result_t *result)
+{
+ apr_status_t rv;
+ md_t *md;
+ md_credentials_t *creds;
+ md_pkey_spec_t *pkspec;
+ apr_array_header_t *all_creds;
+ const char *name;
+ int i;
+
+ name = d->md->name;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name);
+ /* Load data from MD_SG_STAGING and save it into "load_group".
+ */
+ if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) {
+ md_result_set(result, rv, "loading staged md.json");
+ goto leave;
+ }
+
+ /* tailscale generates one cert+key with key specification being whatever
+ * it chooses. Use the NULL spec here.
+ */
+ all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*));
+ pkspec = NULL;
+ if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) {
+ md_result_printf(result, rv, "loading staged credentials");
+ goto leave;
+ }
+ if (!creds->chain) {
+ rv = APR_ENOENT;
+ md_result_printf(result, rv, "no certificate in staged credentials");
+ goto leave;
+ }
+ if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) {
+ md_result_printf(result, rv, "certificate and private key do not match in staged credentials");
+ goto leave;
+ }
+ APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds;
+
+ md_result_activity_setn(result, "purging store tmp space");
+ rv = md_store_purge(d->store, d->p, load_group, name);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, NULL);
+ goto leave;
+ }
+
+ md_result_activity_setn(result, "saving staged md/privkey/pubcert");
+ if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) {
+ md_result_set(result, rv, "writing md.json");
+ goto leave;
+ }
+
+ for (i = 0; i < all_creds->nelts; ++i) {
+ creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*);
+ if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) {
+ md_result_printf(result, rv, "writing credentials #%d", i);
+ goto leave;
+ }
+ }
+
+ md_result_set(result, APR_SUCCESS, "saved staged data successfully");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t rv_of_response(const md_http_response_t *res)
+{
+ switch (res->status) {
+ case 200:
+ return APR_SUCCESS;
+ case 400:
+ return APR_EINVAL;
+ case 401: /* sectigo returns this instead of 403 */
+ case 403:
+ return APR_EACCES;
+ case 404:
+ return APR_ENOENT;
+ default:
+ return APR_EGENERAL;
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t on_get_cert(const md_http_response_t *res, void *baton)
+{
+ ts_ctx_t *ts_ctx = baton;
+ apr_status_t rv;
+
+ rv = rv_of_response(res);
+ if (APR_SUCCESS != rv) goto leave;
+ apr_array_clear(ts_ctx->chain);
+ rv = md_cert_chain_read_http(ts_ctx->chain, ts_ctx->pool, res);
+ if (APR_SUCCESS != rv) goto leave;
+
+leave:
+ return rv;
+}
+
+static apr_status_t on_get_key(const md_http_response_t *res, void *baton)
+{
+ ts_ctx_t *ts_ctx = baton;
+ apr_status_t rv;
+
+ rv = rv_of_response(res);
+ if (APR_SUCCESS != rv) goto leave;
+ rv = md_pkey_read_http(&ts_ctx->pkey, ts_ctx->pool, res);
+ if (APR_SUCCESS != rv) goto leave;
+
+leave:
+ return rv;
+}
+
+static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result)
+{
+ const char *name, *domain, *url;
+ apr_status_t rv = APR_ENOENT;
+ ts_ctx_t *ts_ctx = d->baton;
+ md_http_t *http;
+ const md_pubcert_t *pubcert;
+ md_cert_t *old_cert, *new_cert;
+ int reset_staging = d->reset;
+
+ /* "renewing" the certificate from tailscale. Since tailscale has its
+ * own ideas on when to do this, we can only inspect the certificate
+ * it gives us and see if it is different from the current one we have.
+ * (if we have any. first time, lacking a cert, any it gives us is
+ * considered as 'renewed'.)
+ */
+ name = d->md->name;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: renewing cert", name);
+
+ /* When not explicitly told to reset, we check the existing data. If
+ * it is incomplete or old, we trigger the reset for a clean start. */
+ if (!reset_staging) {
+ md_result_activity_setn(result, "Checking staging area");
+ rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ts_ctx->md, d->p);
+ if (APR_SUCCESS == rv) {
+ /* So, we have a copy in staging, but is it a recent or an old one? */
+ if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) {
+ reset_staging = 1;
+ }
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ reset_staging = 1;
+ rv = APR_SUCCESS;
+ }
+ }
+
+ if (reset_staging) {
+ md_result_activity_setn(result, "Resetting staging area");
+ /* reset the staging area for this domain */
+ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p,
+ "%s: reset staging area", d->md->name);
+ if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) {
+ md_result_printf(result, rv, "resetting staging area");
+ goto leave;
+ }
+ rv = APR_SUCCESS;
+ ts_ctx->md = NULL;
+ }
+
+ if (!ts_ctx->md || !md_array_str_eq(ts_ctx->md->ca_urls, d->md->ca_urls, 1)) {
+ md_result_activity_printf(result, "Resetting staging for %s", d->md->name);
+ /* re-initialize staging */
+ 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);
+ ts_ctx->md = md_copy(d->p, d->md);
+ rv = md_save(d->store, d->p, MD_SG_STAGING, ts_ctx->md, 0);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Saving MD information in staging area.");
+ md_result_log(result, MD_LOG_ERR);
+ goto leave;
+ }
+ }
+
+ if (!ts_ctx->unix_socket_path) {
+ rv = APR_ENOTIMPL;
+ md_result_set(result, rv, "only unix sockets are supported for tailscale connections");
+ goto leave;
+ }
+
+ rv = md_util_is_unix_socket(ts_ctx->unix_socket_path, d->p);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "tailscale socket not available, may not be up: %s",
+ ts_ctx->unix_socket_path);
+ goto leave;
+ }
+
+ rv = md_http_create(&http, d->p,
+ apr_psprintf(d->p, "Apache mod_md/%s", MOD_MD_VERSION),
+ NULL);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "creating http context");
+ goto leave;
+ }
+ md_http_set_unix_socket_path(http, ts_ctx->unix_socket_path);
+
+ domain = (d->md->domains->nelts > 0)?
+ APR_ARRAY_IDX(d->md->domains, 0, const char*) : NULL;
+ if (!domain) {
+ rv = APR_EINVAL;
+ md_result_set(result, rv, "no domain names available");
+ }
+
+ url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=crt",
+ domain);
+ rv = md_http_GET_perform(http, url, NULL, on_get_cert, ts_ctx);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "retrieving certificate from tailscale");
+ goto leave;
+ }
+ if (ts_ctx->chain->nelts <= 0) {
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "tailscale returned no certificates");
+ goto leave;
+ }
+
+ /* Got the key and the chain, is it new? */
+ rv = md_reg_get_pubcert(&pubcert, d->reg,d->md, 0, d->p);
+ if (APR_SUCCESS == rv) {
+ old_cert = APR_ARRAY_IDX(pubcert->certs, 0, md_cert_t*);
+ new_cert = APR_ARRAY_IDX(ts_ctx->chain, 0, md_cert_t*);
+ if (md_certs_are_equal(old_cert, new_cert)) {
+ /* tailscale has not renewed the certificate, yet */
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "tailscale has not renewed the certificate yet");
+ /* let's check this daily */
+ md_result_delay_set(result, apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY));
+ goto leave;
+ }
+ }
+
+ /* We have a new certificate (or had none before).
+ * Get the key and store both in STAGING.
+ */
+ url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=key",
+ domain);
+ rv = md_http_GET_perform(http, url, NULL, on_get_key, ts_ctx);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "retrieving key from tailscale");
+ goto leave;
+ }
+
+ rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, name, NULL, ts_ctx->pkey, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "saving private key");
+ goto leave;
+ }
+
+ rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, name,
+ NULL, ts_ctx->chain, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "saving new certificate chain.");
+ goto leave;
+ }
+
+ md_result_set(result, APR_SUCCESS,
+ "A new tailscale certificate has been retrieved successfully and can "
+ "be used. A graceful server restart is recommended.");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p)
+{
+ (void)p;
+ if (!md->ca_urls) {
+ md->ca_urls = apr_array_make(p, 3, sizeof(const char *));
+ APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_TAILSCALE_DEF_URL;
+ }
+ return APR_SUCCESS;
+}
+
+
+static md_proto_t TAILSCALE_PROTO = {
+ MD_PROTO_TAILSCALE, ts_init, ts_renew,
+ ts_preload_init, ts_preload, ts_complete_md,
+};
+
+apr_status_t md_tailscale_protos_add(apr_hash_t *protos, apr_pool_t *p)
+{
+ (void)p;
+ apr_hash_set(protos, MD_PROTO_TAILSCALE, sizeof(MD_PROTO_TAILSCALE)-1, &TAILSCALE_PROTO);
+ return APR_SUCCESS;
+}
diff --git a/modules/md/md_tailscale.h b/modules/md/md_tailscale.h
new file mode 100644
index 0000000..67a874d
--- /dev/null
+++ b/modules/md/md_tailscale.h
@@ -0,0 +1,25 @@
+/* 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_tailscale_h
+#define mod_md_md_tailscale_h
+
+#define MD_PROTO_TAILSCALE "tailscale"
+
+apr_status_t md_tailscale_protos_add(struct apr_hash_t *protos, apr_pool_t *p);
+
+#endif /* mod_md_md_tailscale_h */
+
diff --git a/modules/md/md_time.c b/modules/md/md_time.c
new file mode 100644
index 0000000..268ca83
--- /dev/null
+++ b/modules/md/md_time.c
@@ -0,0 +1,325 @@
+/* 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 <stdio.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_time.h>
+
+#include "md.h"
+#include "md_time.h"
+
+apr_time_t md_timeperiod_length(const md_timeperiod_t *period)
+{
+ return (period->start < period->end)? (period->end - period->start) : 0;
+}
+
+int md_timeperiod_contains(const md_timeperiod_t *period, apr_time_t time)
+{
+ return md_timeperiod_has_started(period, time)
+ && !md_timeperiod_has_ended(period, time);
+}
+
+int md_timeperiod_has_started(const md_timeperiod_t *period, apr_time_t time)
+{
+ return (time >= period->start);
+}
+
+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];
+ char tend[APR_RFC822_DATE_LEN];
+
+ apr_rfc822_date(tstart, period->start);
+ apr_rfc822_date(tend, period->end);
+ return apr_pstrcat(p, tstart, " - ", tend, NULL);
+}
+
+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, "%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%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%d minutes", s, sep, minutes);
+ if (roughly) return s;
+ sep = " ";
+ }
+ if (rem > 0) {
+ s = apr_psprintf(p, "%s%s%d seconds", s, sep, rem);
+ if (roughly) return s;
+ sep = " ";
+ }
+ }
+ }
+ else if (days == 0) {
+ s = "0 seconds";
+ if (duration != 0) {
+ s = apr_psprintf(p, "%d ms", (int)apr_time_msec(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";
+ int units = (int)(apr_time_sec(duration) / MD_SECS_PER_DAY);
+ int rem = (int)(apr_time_sec(duration) % MD_SECS_PER_DAY);
+
+ if (rem == 0) {
+ s = apr_psprintf(p, "%dd", units);
+ }
+ else {
+ units = (int)(apr_time_sec(duration) / MD_SECS_PER_HOUR);
+ rem = (int)(apr_time_sec(duration) % MD_SECS_PER_HOUR);
+ if (rem == 0) {
+ s = apr_psprintf(p, "%dh", units);
+ }
+ else {
+ units = (int)(apr_time_sec(duration) / 60);
+ rem = (int)(apr_time_sec(duration) % 60);
+ if (rem == 0) {
+ s = apr_psprintf(p, "%dmi", units);
+ }
+ else {
+ units = (int)(apr_time_sec(duration));
+ rem = (int)(apr_time_msec(duration) % 1000);
+ if (rem == 0) {
+ s = apr_psprintf(p, "%ds", units);
+ }
+ else {
+ s = apr_psprintf(p, "%dms", (int)(apr_time_msec(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)
+{
+ char *endp;
+ apr_int64_t n;
+
+ n = apr_strtoi64(value, &endp, 10);
+ if (errno) {
+ return errno;
+ }
+ if (!endp || !*endp) {
+ if (!def_unit) def_unit = "s";
+ }
+ else if (endp == value) {
+ return APR_EINVAL;
+ }
+ else {
+ def_unit = endp;
+ }
+
+ switch (*def_unit) {
+ case 'D':
+ case 'd':
+ *ptimeout = apr_time_from_sec(n * MD_SECS_PER_DAY);
+ break;
+ case 's':
+ case 'S':
+ *ptimeout = (apr_interval_time_t) apr_time_from_sec(n);
+ break;
+ case 'h':
+ case 'H':
+ /* Time is in hours */
+ *ptimeout = (apr_interval_time_t) apr_time_from_sec(n * MD_SECS_PER_HOUR);
+ break;
+ case 'm':
+ case 'M':
+ switch (*(++def_unit)) {
+ /* Time is in milliseconds */
+ case 's':
+ case 'S':
+ *ptimeout = (apr_interval_time_t) n * 1000;
+ break;
+ /* Time is in minutes */
+ case 'i':
+ case 'I':
+ *ptimeout = (apr_interval_time_t) apr_time_from_sec(n * 60);
+ break;
+ default:
+ return APR_EGENERAL;
+ }
+ break;
+ default:
+ return APR_EGENERAL;
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t percentage_parse(const char *value, int *ppercent)
+{
+ char *endp;
+ apr_int64_t n;
+
+ n = apr_strtoi64(value, &endp, 10);
+ if (errno) {
+ return errno;
+ }
+ if (*endp == '%') {
+ if (n < 0) {
+ return APR_BADARG;
+ }
+ *ppercent = (int)n;
+ return APR_SUCCESS;
+ }
+ return APR_EINVAL;
+}
+
+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;
+
+ ts = apr_pcalloc(p, sizeof(*ts));
+ ts->norm = norm;
+ ts->len = len;
+ *pts = ts;
+ return APR_SUCCESS;
+}
+
+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;
+ int percent = 0;
+
+ *pts = NULL;
+ if (!val) {
+ return "cannot parse NULL value";
+ }
+
+ ts = apr_pcalloc(p, sizeof(*ts));
+ if (md_duration_parse(&ts->len, val, "d") == APR_SUCCESS) {
+ *pts = ts;
+ return NULL;
+ }
+ else {
+ switch (percentage_parse(val, &percent)) {
+ case APR_SUCCESS:
+ ts->norm = norm;
+ ts->len = apr_time_from_sec((apr_time_sec(norm) * percent / 100L));
+ *pts = ts;
+ return NULL;
+ case APR_BADARG:
+ return "percent must be less than 100";
+ }
+ }
+ return "has unrecognized format";
+}
+
+const char *md_timeslice_format(const md_timeslice_t *ts, apr_pool_t *p) {
+ if (ts->norm > 0) {
+ int percent = (int)(((long)apr_time_sec(ts->len)) * 100L
+ / ((long)apr_time_sec(ts->norm)));
+ return apr_psprintf(p, "%d%%", percent);
+ }
+ return duration_format(p, ts->len);
+}
+
+md_timeperiod_t md_timeperiod_slice_before_end(const md_timeperiod_t *period,
+ const md_timeslice_t *ts)
+{
+ md_timeperiod_t r;
+ apr_time_t duration = ts->len;
+
+ if (ts->norm > 0) {
+ int percent = (int)(((long)apr_time_sec(ts->len)) * 100L
+ / ((long)apr_time_sec(ts->norm)));
+ apr_time_t plen = md_timeperiod_length(period);
+ if (apr_time_sec(plen) > 100) {
+ duration = apr_time_from_sec(apr_time_sec(plen) * percent / 100);
+ }
+ else {
+ duration = plen * percent / 100;
+ }
+ }
+ r.start = period->end - duration;
+ r.end = period->end;
+ return r;
+}
+
+int md_timeslice_eq(const md_timeslice_t *ts1, const md_timeslice_t *ts2)
+{
+ if (ts1 == ts2) return 1;
+ if (!ts1 || !ts2) return 0;
+ return (ts1->norm == ts2->norm) && (ts1->len == ts2->len);
+}
+
+md_timeperiod_t md_timeperiod_common(const md_timeperiod_t *a, const md_timeperiod_t *b)
+{
+ md_timeperiod_t c;
+
+ c.start = (a->start > b->start)? a->start : b->start;
+ c.end = (a->end < b->end)? a->end : b->end;
+ if (c.start > c.end) {
+ c.start = c.end = 0;
+ }
+ return c;
+}
diff --git a/modules/md/md_time.h b/modules/md/md_time.h
new file mode 100644
index 0000000..92bd9d8
--- /dev/null
+++ b/modules/md/md_time.h
@@ -0,0 +1,77 @@
+/* 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_time_h
+#define mod_md_md_time_h
+
+#include <stdio.h>
+
+#define MD_SECS_PER_HOUR (60*60)
+#define MD_SECS_PER_DAY (24*MD_SECS_PER_HOUR)
+
+typedef struct md_timeperiod_t md_timeperiod_t;
+
+struct md_timeperiod_t {
+ apr_time_t start;
+ apr_time_t end;
+};
+
+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);
+
+/**
+ * Return the timeperiod common between a and b. If both do not overlap, return {0,0}.
+ */
+md_timeperiod_t md_timeperiod_common(const md_timeperiod_t *a, const md_timeperiod_t *b);
+
+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
+ * unit is d/h/mi/s/ms with the default given should the unit not be specified.
+ */
+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(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(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);
+
+md_timeperiod_t md_timeperiod_slice_before_end(const md_timeperiod_t *period,
+ const md_timeslice_t *ts);
+
+#endif /* md_util_h */
diff --git a/modules/md/md_util.c b/modules/md/md_util.c
new file mode 100644
index 0000000..884c0bb
--- /dev/null
+++ b/modules/md/md_util.c
@@ -0,0 +1,1561 @@
+/* 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 <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_portable.h>
+#include <apr_file_info.h>
+#include <apr_fnmatch.h>
+#include <apr_tables.h>
+#include <apr_uri.h>
+
+#if APR_HAVE_STDLIB_H
+#include <stdlib.h>
+#endif
+
+#include "md.h"
+#include "md_log.h"
+#include "md_util.h"
+
+/**************************************************************************************************/
+/* pool utils */
+
+apr_status_t md_util_pool_do(md_util_action *cb, void *baton, apr_pool_t *p)
+{
+ apr_pool_t *ptemp;
+ apr_status_t rv = apr_pool_create(&ptemp, p);
+ if (APR_SUCCESS == rv) {
+ apr_pool_tag(ptemp, "md_pool_do");
+ rv = cb(baton, p, ptemp);
+ apr_pool_destroy(ptemp);
+ }
+ return rv;
+}
+
+static apr_status_t pool_vado(md_util_vaction *cb, void *baton, apr_pool_t *p, va_list ap)
+{
+ apr_pool_t *ptemp;
+ apr_status_t rv;
+
+ rv = apr_pool_create(&ptemp, p);
+ if (APR_SUCCESS == rv) {
+ apr_pool_tag(ptemp, "md_pool_vado");
+ rv = cb(baton, p, ptemp, ap);
+ apr_pool_destroy(ptemp);
+ }
+ return rv;
+}
+
+apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, ...)
+{
+ va_list ap;
+ apr_status_t rv;
+
+ va_start(ap, p);
+ rv = pool_vado(cb, baton, p, ap);
+ va_end(ap);
+ return rv;
+}
+
+/**************************************************************************************************/
+/* data chunks */
+
+void md_data_pinit(md_data_t *d, apr_size_t len, apr_pool_t *p)
+{
+ md_data_null(d);
+ d->data = apr_pcalloc(p, len);
+ d->len = len;
+}
+
+md_data_t *md_data_pmake(apr_size_t len, apr_pool_t *p)
+{
+ md_data_t *d;
+
+ d = apr_palloc(p, sizeof(*d));
+ md_data_pinit(d, len, p);
+ return d;
+}
+
+void md_data_init(md_data_t *d, const char *data, apr_size_t len)
+{
+ md_data_null(d);
+ d->len = len;
+ d->data = data;
+}
+
+void md_data_init_str(md_data_t *d, const char *str)
+{
+ md_data_init(d, str, strlen(str));
+}
+
+void md_data_null(md_data_t *d)
+{
+ memset(d, 0, sizeof(*d));
+}
+
+void md_data_clear(md_data_t *d)
+{
+ if (d) {
+ if (d->data && d->free_data) d->free_data((void*)d->data);
+ memset(d, 0, sizeof(*d));
+ }
+}
+
+md_data_t *md_data_make_pcopy(apr_pool_t *p, const char *data, apr_size_t len)
+{
+ md_data_t *d;
+
+ d = apr_palloc(p, sizeof(*d));
+ d->len = len;
+ d->data = len? apr_pmemdup(p, data, len) : NULL;
+ return d;
+}
+
+apr_status_t md_data_assign_copy(md_data_t *dest, const char *src, apr_size_t src_len)
+{
+ md_data_clear(dest);
+ if (src && src_len) {
+ dest->data = malloc(src_len);
+ if (!dest->data) return APR_ENOMEM;
+ memcpy((void*)dest->data, src, src_len);
+ dest->len = src_len;
+ dest->free_data = free;
+ }
+ return APR_SUCCESS;
+}
+
+void md_data_assign_pcopy(md_data_t *dest, const char *src, apr_size_t src_len, apr_pool_t *p)
+{
+ md_data_clear(dest);
+ dest->data = (src && src_len)? apr_pmemdup(p, src, 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",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f",
+ "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f",
+ "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f",
+ "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f",
+ "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f",
+ "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f",
+ "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f",
+ "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af",
+ "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf",
+ "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf",
+ "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df",
+ "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef",
+ "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff",
+};
+
+apr_status_t md_data_to_hex(const char **phex, char separator,
+ apr_pool_t *p, const md_data_t *data)
+{
+ char *hex, *cp;
+ const char * x;
+ unsigned int i;
+
+ cp = hex = apr_pcalloc(p, ((separator? 3 : 2) * data->len) + 1);
+ if (!hex) {
+ *phex = NULL;
+ return APR_ENOMEM;
+ }
+ for (i = 0; i < data->len; ++i) {
+ x = hex_const[(unsigned char)data->data[i]];
+ if (i && separator) *cp++ = separator;
+ *cp++ = x[0];
+ *cp++ = x[1];
+ }
+ *phex = hex;
+ return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* 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, (size_t)((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)
+{
+ return (array == NULL) || (array->nelts == 0);
+}
+
+char *md_util_str_tolower(char *s)
+{
+ char *orig = s;
+ while (*s) {
+ *s = (char)apr_tolower(*s);
+ ++s;
+ }
+ return orig;
+}
+
+int md_array_str_index(const apr_array_header_t *array, const char *s,
+ int start, int case_sensitive)
+{
+ if (start >= 0) {
+ int i;
+
+ for (i = start; i < array->nelts; i++) {
+ const char *p = APR_ARRAY_IDX(array, i, const char *);
+ if ((case_sensitive && !strcmp(p, s))
+ || (!case_sensitive && !apr_strnatcasecmp(p, s))) {
+ return i;
+ }
+ }
+ }
+
+ return -1;
+}
+
+int md_array_str_eq(const struct apr_array_header_t *a1,
+ const struct apr_array_header_t *a2, int case_sensitive)
+{
+ int i;
+ const char *s1, *s2;
+
+ if (a1 == a2) return 1;
+ if (!a1 || !a2) return 0;
+ if (a1->nelts != a2->nelts) return 0;
+ for (i = 0; i < a1->nelts; ++i) {
+ s1 = APR_ARRAY_IDX(a1, i, const char *);
+ s2 = APR_ARRAY_IDX(a2, i, const char *);
+ if ((case_sensitive && strcmp(s1, s2))
+ || (!case_sensitive && apr_strnatcasecmp(s1, s2))) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+apr_array_header_t *md_array_str_clone(apr_pool_t *p, apr_array_header_t *src)
+{
+ apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*));
+ if (dest) {
+ int i;
+ for (i = 0; i < src->nelts; i++) {
+ const char *s = APR_ARRAY_IDX(src, i, const char *);
+ APR_ARRAY_PUSH(dest, const char *) = apr_pstrdup(p, s);
+ }
+ }
+ return dest;
+}
+
+struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src,
+ int case_sensitive)
+{
+ apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*));
+ if (dest) {
+ const char *s;
+ int i;
+ for (i = 0; i < src->nelts; ++i) {
+ s = APR_ARRAY_IDX(src, i, const char *);
+ if (md_array_str_index(dest, s, 0, case_sensitive) < 0) {
+ APR_ARRAY_PUSH(dest, char *) = md_util_str_tolower(apr_pstrdup(p, s));
+ }
+ }
+ }
+ return dest;
+}
+
+apr_array_header_t *md_array_str_remove(apr_pool_t *p, apr_array_header_t *src,
+ const char *exclude, int case_sensitive)
+{
+ apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*));
+ if (dest) {
+ int i;
+ for (i = 0; i < src->nelts; i++) {
+ const char *s = APR_ARRAY_IDX(src, i, const char *);
+ if (!exclude
+ || (case_sensitive && strcmp(exclude, s))
+ || (!case_sensitive && apr_strnatcasecmp(exclude, s))) {
+ APR_ARRAY_PUSH(dest, const char *) = apr_pstrdup(p, s);
+ }
+ }
+ }
+ return dest;
+}
+
+int md_array_str_add_missing(apr_array_header_t *dest, apr_array_header_t *src, int case_sensitive)
+{
+ int i, added = 0;
+ for (i = 0; i < src->nelts; i++) {
+ const char *s = APR_ARRAY_IDX(src, i, const char *);
+ if (md_array_str_index(dest, s, 0, case_sensitive) < 0) {
+ APR_ARRAY_PUSH(dest, const char *) = s;
+ ++added;
+ }
+ }
+ return added;
+}
+
+/**************************************************************************************************/
+/* file system related */
+
+apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode)
+{
+ *pf = fopen(fn, mode);
+ if (*pf == NULL) {
+ return errno;
+ }
+
+ return APR_SUCCESS;
+}
+
+apr_status_t md_util_fcreatex(apr_file_t **pf, const char *fn,
+ apr_fileperms_t perms, apr_pool_t *p)
+{
+ apr_status_t rv;
+ rv = apr_file_open(pf, fn, (APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_EXCL),
+ perms, p);
+ if (APR_SUCCESS == rv) {
+ /* See <https://github.com/icing/mod_md/issues/117>
+ * Some people set umask 007 to deny all world read/writability to files
+ * created by apache. While this is a noble effort, we need the store files
+ * to have the permissions as specified. */
+ rv = apr_file_perms_set(fn, perms);
+ if (APR_STATUS_IS_ENOTIMPL(rv)) {
+ rv = APR_SUCCESS;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool)
+{
+ apr_finfo_t info;
+ apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool);
+ if (rv == APR_SUCCESS) {
+ rv = (info.filetype == APR_DIR)? APR_SUCCESS : APR_EINVAL;
+ }
+ return rv;
+}
+
+apr_status_t md_util_is_file(const char *path, apr_pool_t *pool)
+{
+ apr_finfo_t info;
+ apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool);
+ if (rv == APR_SUCCESS) {
+ rv = (info.filetype == APR_REG)? APR_SUCCESS : APR_EINVAL;
+ }
+ return rv;
+}
+
+apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool)
+{
+ apr_finfo_t info;
+ apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool);
+ if (rv == APR_SUCCESS) {
+ rv = (info.filetype == APR_SOCK)? APR_SUCCESS : APR_EINVAL;
+ }
+ return rv;
+}
+
+int md_file_exists(const char *fname, apr_pool_t *p)
+{
+ return (fname && *fname && APR_SUCCESS == md_util_is_file(fname, p));
+}
+
+apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...)
+{
+ const char *segment, *path;
+ va_list ap;
+ apr_status_t rv = APR_SUCCESS;
+
+ va_start(ap, p);
+ path = va_arg(ap, char *);
+ while (path && APR_SUCCESS == rv && (segment = va_arg(ap, char *))) {
+ rv = apr_filepath_merge((char **)&path, path, segment, APR_FILEPATH_SECUREROOT , p);
+ }
+ va_end(ap);
+
+ *ppath = (APR_SUCCESS == rv)? (path? path : "") : NULL;
+ return rv;
+}
+
+apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool_t *p,
+ md_util_file_cb *write_cb, void *baton)
+{
+ apr_status_t rv = APR_EEXIST;
+ apr_file_t *f;
+ const char *tmp;
+ int i, max;
+
+ tmp = apr_psprintf(p, "%s.tmp", fpath);
+ i = 0; max = 20;
+creat:
+ while (i < max && APR_EEXIST == (rv = md_util_fcreatex(&f, tmp, perms, p))) {
+ ++i;
+ apr_sleep(apr_time_from_msec(50));
+ }
+ if (APR_EEXIST == rv
+ && APR_SUCCESS == (rv = apr_file_remove(tmp, p))
+ && max <= 20) {
+ max *= 2;
+ goto creat;
+ }
+
+ if (APR_SUCCESS == rv) {
+ rv = write_cb(baton, f, p);
+ apr_file_close(f);
+
+ if (APR_SUCCESS == rv) {
+ rv = apr_file_rename(tmp, fpath, p);
+ if (APR_SUCCESS != rv) {
+ apr_file_remove(tmp, p);
+ }
+ }
+ }
+ return rv;
+}
+
+/**************************************************************************************************/
+/* text files */
+
+apr_status_t md_text_fread8k(const char **ptext, apr_pool_t *p, const char *fpath)
+{
+ apr_status_t rv;
+ apr_file_t *f;
+ char buffer[8 * 1024];
+
+ *ptext = NULL;
+ if (APR_SUCCESS == (rv = apr_file_open(&f, fpath, APR_FOPEN_READ, 0, p))) {
+ apr_size_t blen = sizeof(buffer)/sizeof(buffer[0]) - 1;
+ rv = apr_file_read_full(f, buffer, blen, &blen);
+ if (APR_SUCCESS == rv || APR_STATUS_IS_EOF(rv)) {
+ *ptext = apr_pstrndup(p, buffer, blen);
+ rv = APR_SUCCESS;
+ }
+ apr_file_close(f);
+ }
+ return rv;
+}
+
+static apr_status_t write_text(void *baton, struct apr_file_t *f, apr_pool_t *p)
+{
+ const char *text = baton;
+ apr_size_t len = strlen(text);
+
+ (void)p;
+ return apr_file_write_full(f, text, len, &len);
+}
+
+apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t perms,
+ apr_pool_t *p, const char *text)
+{
+ apr_status_t rv;
+ apr_file_t *f;
+
+ rv = md_util_fcreatex(&f, fpath, perms, p);
+ if (APR_SUCCESS == rv) {
+ rv = write_text((void*)text, f, p);
+ apr_file_close(f);
+ /* See <https://github.com/icing/mod_md/issues/117>: when a umask
+ * is set, files need to be assigned permissions explicitly.
+ * Otherwise, as in the issues reported, it will break our access model. */
+ rv = apr_file_perms_set(fpath, perms);
+ if (APR_STATUS_IS_ENOTIMPL(rv)) {
+ rv = APR_SUCCESS;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms,
+ apr_pool_t *p, const char *text)
+{
+ return md_util_freplace(fpath, perms, p, write_text, (void*)text);
+}
+
+typedef struct {
+ const char *path;
+ apr_array_header_t *patterns;
+ int follow_links;
+ void *baton;
+ md_util_fdo_cb *cb;
+} md_util_fwalk_t;
+
+static apr_status_t rm_recursive(const char *fpath, apr_pool_t *p, int max_level)
+{
+ apr_finfo_t info;
+ apr_status_t rv;
+ const char *npath;
+
+ if (APR_SUCCESS != (rv = apr_stat(&info, fpath, (APR_FINFO_TYPE|APR_FINFO_LINK), p))) {
+ return rv;
+ }
+
+ if (info.filetype == APR_DIR) {
+ if (max_level > 0) {
+ apr_dir_t *d;
+
+ if (APR_SUCCESS == (rv = apr_dir_open(&d, fpath, p))) {
+
+ while (APR_SUCCESS == rv &&
+ APR_SUCCESS == (rv = apr_dir_read(&info, APR_FINFO_TYPE, d))) {
+ if (!strcmp(".", info.name) || !strcmp("..", info.name)) {
+ continue;
+ }
+
+ rv = md_util_path_merge(&npath, p, fpath, info.name, NULL);
+ if (APR_SUCCESS == rv) {
+ rv = rm_recursive(npath, p, max_level - 1);
+ }
+ }
+ apr_dir_close(d);
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ }
+ }
+ }
+ if (APR_SUCCESS == rv) {
+ rv = apr_dir_remove(fpath, p);
+ }
+ }
+ else {
+ rv = apr_file_remove(fpath, p);
+ }
+ return rv;
+}
+
+static apr_status_t prm_recursive(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ int max_level = va_arg(ap, int);
+
+ (void)p;
+ return rm_recursive(baton, ptemp, max_level);
+}
+
+apr_status_t md_util_rm_recursive(const char *fpath, apr_pool_t *p, int max_level)
+{
+ return md_util_pool_vdo(prm_recursive, (void*)fpath, p, max_level, NULL);
+}
+
+static apr_status_t match_and_do(md_util_fwalk_t *ctx, const char *path, int depth,
+ apr_pool_t *p, apr_pool_t *ptemp)
+{
+ apr_status_t rv = APR_SUCCESS;
+ const char *pattern, *npath;
+ apr_dir_t *d;
+ apr_finfo_t finfo;
+ int ndepth = depth + 1;
+ apr_int32_t wanted = (APR_FINFO_TYPE);
+
+ if (depth >= ctx->patterns->nelts) {
+ return APR_SUCCESS;
+ }
+ pattern = APR_ARRAY_IDX(ctx->patterns, depth, const char *);
+
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do "
+ "path=%s depth=%d pattern=%s", path, depth, pattern);
+ rv = apr_dir_open(&d, path, ptemp);
+ if (APR_SUCCESS != rv) {
+ return rv;
+ }
+
+ while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do "
+ "candidate=%s", finfo.name);
+ if (!strcmp(".", finfo.name) || !strcmp("..", finfo.name)) {
+ continue;
+ }
+ if (APR_SUCCESS == apr_fnmatch(pattern, finfo.name, 0)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do "
+ "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 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);
+ if (APR_SUCCESS == rv) {
+ rv = match_and_do(ctx, npath, ndepth, p, ptemp);
+ }
+ }
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do "
+ "invoking inspector on name=%s", finfo.name);
+ rv = ctx->cb(ctx->baton, p, ptemp, path, finfo.name, finfo.filetype);
+ }
+ }
+ if (APR_SUCCESS != rv) {
+ break;
+ }
+ }
+
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ }
+
+ apr_dir_close(d);
+ return rv;
+}
+
+static apr_status_t files_do_start(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+ md_util_fwalk_t *ctx = baton;
+ const char *segment;
+
+ ctx->patterns = apr_array_make(ptemp, 5, sizeof(const char*));
+
+ segment = va_arg(ap, char *);
+ while (segment) {
+ APR_ARRAY_PUSH(ctx->patterns, const char *) = segment;
+ segment = va_arg(ap, char *);
+ }
+
+ return match_and_do(ctx, ctx->path, 0, p, ptemp);
+}
+
+apr_status_t md_util_files_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p,
+ const char *path, ...)
+{
+ apr_status_t rv;
+ va_list ap;
+ md_util_fwalk_t ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.path = path;
+ ctx.follow_links = 1;
+ ctx.cb = cb;
+ ctx.baton = baton;
+
+ va_start(ap, path);
+ rv = pool_vado(files_do_start, &ctx, p, ap);
+ va_end(ap);
+
+ return rv;
+}
+
+static apr_status_t tree_do(void *baton, apr_pool_t *p, apr_pool_t *ptemp, const char *path)
+{
+ md_util_fwalk_t *ctx = baton;
+
+ apr_status_t rv = APR_SUCCESS;
+ const char *name, *fpath;
+ apr_filetype_e ftype;
+ apr_dir_t *d;
+ apr_int32_t wanted = APR_FINFO_TYPE;
+ apr_finfo_t finfo;
+
+ if (APR_SUCCESS == (rv = apr_dir_open(&d, path, ptemp))) {
+ while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) {
+ name = finfo.name;
+ if (!strcmp(".", name) || !strcmp("..", name)) {
+ continue;
+ }
+
+ fpath = NULL;
+ ftype = finfo.filetype;
+
+ if (APR_LNK == ftype && ctx->follow_links) {
+ rv = md_util_path_merge(&fpath, ptemp, path, name, NULL);
+ if (APR_SUCCESS == rv) {
+ rv = apr_stat(&finfo, ctx->path, wanted, ptemp);
+ }
+ }
+
+ if (APR_DIR == finfo.filetype) {
+ if (!fpath) {
+ rv = md_util_path_merge(&fpath, ptemp, path, name, NULL);
+ }
+ if (APR_SUCCESS == rv) {
+ rv = tree_do(ctx, p, ptemp, fpath);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "dir cb(%s/%s)",
+ path, name);
+ rv = ctx->cb(ctx->baton, p, ptemp, path, name, ftype);
+ }
+ }
+ else {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "file cb(%s/%s)",
+ path, name);
+ rv = ctx->cb(ctx->baton, p, ptemp, path, name, finfo.filetype);
+ }
+ }
+
+ apr_dir_close(d);
+
+ if (APR_STATUS_IS_ENOENT(rv)) {
+ rv = APR_SUCCESS;
+ }
+ }
+ return rv;
+}
+
+static apr_status_t tree_start_do(void *baton, apr_pool_t *p, apr_pool_t *ptemp)
+{
+ md_util_fwalk_t *ctx = baton;
+ apr_finfo_t info;
+ apr_status_t rv;
+ apr_int32_t wanted = ctx->follow_links? APR_FINFO_TYPE : (APR_FINFO_TYPE|APR_FINFO_LINK);
+
+ rv = apr_stat(&info, ctx->path, wanted, ptemp);
+ if (rv == APR_SUCCESS) {
+ switch (info.filetype) {
+ case APR_DIR:
+ rv = tree_do(ctx, p, ptemp, ctx->path);
+ break;
+ default:
+ rv = APR_EINVAL;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_util_tree_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p,
+ const char *path, int follow_links)
+{
+ apr_status_t rv;
+ md_util_fwalk_t ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.path = path;
+ ctx.follow_links = follow_links;
+ ctx.cb = cb;
+ ctx.baton = baton;
+
+ rv = md_util_pool_do(tree_start_do, &ctx, p);
+
+ return rv;
+}
+
+static apr_status_t rm_cb(void *baton, apr_pool_t *p, apr_pool_t *ptemp,
+ const char *path, const char *name, apr_filetype_e ftype)
+{
+ apr_status_t rv;
+ const char *fpath;
+
+ (void)baton;
+ (void)p;
+ rv = md_util_path_merge(&fpath, ptemp, path, name, NULL);
+ if (APR_SUCCESS == rv) {
+ if (APR_DIR == ftype) {
+ rv = apr_dir_remove(fpath, ptemp);
+ }
+ else {
+ rv = apr_file_remove(fpath, ptemp);
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p)
+{
+ apr_status_t rv = md_util_tree_do(rm_cb, NULL, p, path, 0);
+ if (APR_SUCCESS == rv) {
+ rv = apr_dir_remove(path, p);
+ }
+ return rv;
+}
+
+/* DNS name checks ********************************************************************************/
+
+int md_dns_is_name(apr_pool_t *p, const char *hostname, int need_fqdn)
+{
+ char c, last = 0;
+ const char *cp = hostname;
+ int dots = 0;
+
+ /* Since we use the names in certificates, we need pure ASCII domain names
+ * and IDN need to be converted to unicode. */
+ while ((c = *cp++)) {
+ switch (c) {
+ case '.':
+ if (last == '.') {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "dns name with ..: %s",
+ hostname);
+ return 0;
+ }
+ ++dots;
+ break;
+ case '-':
+ break;
+ default:
+ if (!apr_isalnum(c)) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "dns invalid char %c: %s",
+ c, hostname);
+ return 0;
+ }
+ break;
+ }
+ last = c;
+ }
+
+ if (last == '.') { /* DNS names may end with '.' */
+ --dots;
+ }
+ if (need_fqdn && dots <= 0) { /* do not accept just top level domains */
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "not a FQDN: %s", hostname);
+ return 0;
+ }
+ return 1; /* empty string not allowed */
+}
+
+int md_dns_is_wildcard(apr_pool_t *p, const char *domain)
+{
+ if (domain[0] != '*' || domain[1] != '.') return 0;
+ return md_dns_is_name(p, domain+2, 1);
+}
+
+int md_dns_matches(const char *pattern, const char *domain)
+{
+ const char *s;
+
+ if (!apr_strnatcasecmp(pattern, domain)) return 1;
+ if (pattern[0] == '*' && pattern[1] == '.') {
+ s = strchr(domain, '.');
+ if (s && !apr_strnatcasecmp(pattern+1, s)) return 1;
+ }
+ return 0;
+}
+
+apr_array_header_t *md_dns_make_minimal(apr_pool_t *p, apr_array_header_t *domains)
+{
+ apr_array_header_t *minimal;
+ const char *domain, *pattern;
+ int i, j, duplicate;
+
+ minimal = apr_array_make(p, domains->nelts, sizeof(const char *));
+ for (i = 0; i < domains->nelts; ++i) {
+ domain = APR_ARRAY_IDX(domains, i, const char*);
+ duplicate = 0;
+ /* is it matched in minimal already? */
+ for (j = 0; j < minimal->nelts; ++j) {
+ pattern = APR_ARRAY_IDX(minimal, j, const char*);
+ if (md_dns_matches(pattern, domain)) {
+ duplicate = 1;
+ break;
+ }
+ }
+ if (!duplicate) {
+ if (!md_dns_is_wildcard(p, domain)) {
+ /* plain name, will we see a wildcard that replaces it? */
+ for (j = i+1; j < domains->nelts; ++j) {
+ pattern = APR_ARRAY_IDX(domains, j, const char*);
+ if (md_dns_is_wildcard(p, pattern) && md_dns_matches(pattern, domain)) {
+ duplicate = 1;
+ break;
+ }
+ }
+ }
+ if (!duplicate) {
+ APR_ARRAY_PUSH(minimal, const char *) = domain;
+ }
+ }
+ }
+ return minimal;
+}
+
+int md_dns_domains_match(const apr_array_header_t *domains, const char *name)
+{
+ const char *domain;
+ int i;
+
+ for (i = 0; i < domains->nelts; ++i) {
+ domain = APR_ARRAY_IDX(domains, i, const char*);
+ if (md_dns_matches(domain, name)) return 1;
+ }
+ return 0;
+}
+
+const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme)
+{
+ const char *cp = s;
+ while (*cp) {
+ if (*cp == ':') {
+ /* could be an url scheme, leave unchanged */
+ return s;
+ }
+ else if (!apr_isalnum(*cp)) {
+ break;
+ }
+ ++cp;
+ }
+ return apr_psprintf(p, "%s:%s", def_scheme, s);
+}
+
+static apr_status_t uri_check(apr_uri_t *uri_parsed, apr_pool_t *p,
+ const char *uri, const char **perr)
+{
+ const char *s, *err = NULL;
+ apr_status_t rv;
+
+ if (APR_SUCCESS != (rv = apr_uri_parse(p, uri, uri_parsed))) {
+ err = "not an uri";
+ }
+ else if (uri_parsed->scheme) {
+ if (strlen(uri_parsed->scheme) + 1 >= strlen(uri)) {
+ err = "missing uri identifier";
+ }
+ else if (!strncmp("http", uri_parsed->scheme, 4)) {
+ if (!uri_parsed->hostname) {
+ err = "missing hostname";
+ }
+ else if (!md_dns_is_name(p, uri_parsed->hostname, 0)) {
+ err = "invalid hostname";
+ }
+ if (uri_parsed->port_str
+ && (!apr_isdigit(uri_parsed->port_str[0])
+ || uri_parsed->port == 0
+ || uri_parsed->port > 65353)) {
+ err = "invalid port";
+ }
+ }
+ else if (!strcmp("mailto", uri_parsed->scheme)) {
+ s = strchr(uri, '@');
+ if (!s) {
+ err = "missing @";
+ }
+ else if (strchr(s+1, '@')) {
+ err = "duplicate @";
+ }
+ else if (s == uri + strlen(uri_parsed->scheme) + 1) {
+ err = "missing local part";
+ }
+ else if (s == (uri + strlen(uri)-1)) {
+ err = "missing hostname";
+ }
+ else if (strstr(uri, "..")) {
+ err = "double period";
+ }
+ }
+ }
+ if (strchr(uri, ' ') || strchr(uri, '\t') ) {
+ err = "whitespace in uri";
+ }
+
+ if (err) {
+ rv = APR_EINVAL;
+ }
+ *perr = err;
+ return rv;
+}
+
+apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char **perr)
+{
+ apr_uri_t uri_parsed;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) {
+ if (!uri_parsed.scheme) {
+ *perr = "missing uri scheme";
+ return APR_EINVAL;
+ }
+ }
+ return rv;
+}
+
+apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr)
+{
+ apr_uri_t uri_parsed;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) {
+ if (!uri_parsed.scheme) {
+ *perr = "missing uri scheme";
+ return APR_EINVAL;
+ }
+ if (apr_strnatcasecmp("http", uri_parsed.scheme)
+ && apr_strnatcasecmp("https", uri_parsed.scheme)) {
+ *perr = "uri scheme must be http or https";
+ return APR_EINVAL;
+ }
+ }
+ return rv;
+}
+
+/* try and retry for a while **********************************************************************/
+
+apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs,
+ apr_interval_time_t timeout, apr_interval_time_t start_delay,
+ apr_interval_time_t max_delay, int backoff)
+{
+ apr_status_t rv;
+ apr_time_t now = apr_time_now();
+ apr_time_t giveup = now + timeout;
+ apr_interval_time_t nap_duration = start_delay? start_delay : apr_time_from_msec(100);
+ apr_interval_time_t nap_max = max_delay? max_delay : apr_time_from_sec(10);
+ apr_interval_time_t left;
+ int i = 0;
+
+ while (1) {
+ if (APR_SUCCESS == (rv = fn(baton, i++))) {
+ break;
+ }
+ else if (!APR_STATUS_IS_EAGAIN(rv) && !ignore_errs) {
+ break;
+ }
+
+ now = apr_time_now();
+ if (now > giveup) {
+ rv = APR_TIMEUP;
+ break;
+ }
+
+ left = giveup - now;
+ if (nap_duration > left) {
+ nap_duration = left;
+ }
+ if (nap_duration > nap_max) {
+ nap_duration = nap_max;
+ }
+
+ apr_sleep(nap_duration);
+ if (backoff) {
+ nap_duration *= 2;
+ }
+ }
+ return rv;
+}
+
+/* execute process ********************************************************************************/
+
+apr_status_t md_util_exec(apr_pool_t *p, const char *cmd, const char * const *argv,
+ apr_array_header_t *env, int *exit_code)
+{
+ apr_status_t rv;
+ apr_procattr_t *procattr;
+ apr_proc_t *proc;
+ apr_exit_why_e ewhy;
+ const char * const *envp = NULL;
+ char buffer[1024];
+
+ *exit_code = 0;
+ if (!(proc = apr_pcalloc(p, sizeof(*proc)))) {
+ return APR_ENOMEM;
+ }
+ if (env && env->nelts > 0) {
+ apr_array_header_t *nenv;
+
+ nenv = apr_array_copy(p, env);
+ APR_ARRAY_PUSH(nenv, const char *) = NULL;
+ envp = (const char * const *)nenv->elts;
+ }
+ if ( APR_SUCCESS == (rv = apr_procattr_create(&procattr, p))
+ && APR_SUCCESS == (rv = apr_procattr_io_set(procattr, APR_NO_FILE,
+ APR_NO_PIPE, APR_FULL_BLOCK))
+ && APR_SUCCESS == (rv = apr_procattr_cmdtype_set(procattr, APR_PROGRAM))
+ && APR_SUCCESS == (rv = apr_proc_create(proc, cmd, argv, envp, procattr, p))) {
+
+ /* read stderr and log on INFO for possible fault analysis. */
+ while(APR_SUCCESS == (rv = apr_file_gets(buffer, sizeof(buffer)-1, proc->err))) {
+ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "cmd(%s) stderr: %s", cmd, buffer);
+ }
+ if (!APR_STATUS_IS_EOF(rv)) goto out;
+ apr_file_close(proc->err);
+
+ if (APR_CHILD_DONE == (rv = apr_proc_wait(proc, exit_code, &ewhy, APR_WAIT))) {
+ /* let's not dwell on exit stati, but core should signal something's bad */
+ if (*exit_code > 127 || APR_PROC_SIGNAL_CORE == ewhy) {
+ return APR_EINCOMPLETE;
+ }
+ return APR_SUCCESS;
+ }
+ }
+out:
+ return rv;
+}
+
+/* base64 url encoding ****************************************************************************/
+
+#define N6 (unsigned int)-1
+
+static const unsigned int BASE64URL_UINT6[] = {
+/* 0 1 2 3 4 5 6 7 8 9 a b c d e f */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 0 */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 1 */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, 62, N6, N6, /* 2 */
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, N6, N6, N6, N6, N6, N6, /* 3 */
+ N6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 4 */
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, N6, N6, N6, N6, 63, /* 5 */
+ N6, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 6 */
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, N6, N6, N6, N6, N6, /* 7 */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 8 */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 9 */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* a */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* b */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* c */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* d */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* e */
+ N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6 /* f */
+};
+static const unsigned char BASE64URL_CHARS[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', /* 0 - 9 */
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', /* 10 - 19 */
+ 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', /* 20 - 29 */
+ 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', /* 30 - 39 */
+ 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', /* 40 - 49 */
+ 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', /* 50 - 59 */
+ '8', '9', '-', '_', ' ', ' ', ' ', ' ', ' ', ' ', /* 60 - 69 */
+};
+
+#define BASE64URL_CHAR(x) BASE64URL_CHARS[ (unsigned int)(x) & 0x3fu ]
+
+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;
+ const unsigned char *p = e;
+ unsigned char *d;
+ unsigned int n;
+ long len, mlen, remain, i;
+
+ while (*p && BASE64URL_UINT6[ *p ] != N6) {
+ ++p;
+ }
+ len = (int)(p - e);
+ mlen = (len/4)*4;
+ decoded->data = apr_pcalloc(pool, (apr_size_t)len + 1);
+
+ i = 0;
+ d = (unsigned char*)decoded->data;
+ for (; i < mlen; i += 4) {
+ n = ((BASE64URL_UINT6[ e[i+0] ] << 18) +
+ (BASE64URL_UINT6[ e[i+1] ] << 12) +
+ (BASE64URL_UINT6[ e[i+2] ] << 6) +
+ (BASE64URL_UINT6[ e[i+3] ]));
+ *d++ = (unsigned char)(n >> 16);
+ *d++ = (unsigned char)(n >> 8 & 0xffu);
+ *d++ = (unsigned char)(n & 0xffu);
+ }
+ remain = len - mlen;
+ switch (remain) {
+ case 2:
+ n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) +
+ (BASE64URL_UINT6[ e[mlen+1] ] << 12));
+ *d++ = (unsigned char)(n >> 16);
+ remain = 1;
+ break;
+ case 3:
+ n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) +
+ (BASE64URL_UINT6[ e[mlen+1] ] << 12) +
+ (BASE64URL_UINT6[ e[mlen+2] ] << 6));
+ *d++ = (unsigned char)(n >> 16);
+ *d++ = (unsigned char)(n >> 8 & 0xffu);
+ remain = 2;
+ break;
+ default: /* do nothing */
+ break;
+ }
+ decoded->len = (apr_size_t)(mlen/4*3 + remain);
+ return decoded->len;
+}
+
+const char *md_util_base64url_encode(const md_data_t *data, apr_pool_t *pool)
+{
+ 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;
+ for (i = 0; i < len-2; i+= 3) {
+ *p++ = BASE64URL_CHAR( (udata[i] >> 2) );
+ *p++ = BASE64URL_CHAR( (udata[i] << 4) + (udata[i+1] >> 4) );
+ *p++ = BASE64URL_CHAR( (udata[i+1] << 2) + (udata[i+2] >> 6) );
+ *p++ = BASE64URL_CHAR( (udata[i+2]) );
+ }
+
+ if (i < len) {
+ *p++ = BASE64URL_CHAR( (udata[i] >> 2) );
+ if (i == (len - 1)) {
+ *p++ = BASE64URL_CHARS[ ((unsigned int)udata[i] << 4) & 0x3fu ];
+ }
+ else {
+ *p++ = BASE64URL_CHAR( (udata[i] << 4) + (udata[i+1] >> 4) );
+ *p++ = BASE64URL_CHAR( (udata[i+1] << 2) );
+ }
+ }
+ *p++ = '\0';
+ return (char *)enc;
+}
+
+/*******************************************************************************
+ * link header handling
+ ******************************************************************************/
+
+typedef struct {
+ const char *s;
+ apr_size_t slen;
+ apr_size_t i;
+ apr_size_t link_start;
+ apr_size_t link_len;
+ apr_size_t pn_start;
+ apr_size_t pn_len;
+ apr_size_t pv_start;
+ apr_size_t pv_len;
+} link_ctx;
+
+static int attr_char(char c)
+{
+ switch (c) {
+ case '!':
+ case '#':
+ case '$':
+ case '&':
+ case '+':
+ case '-':
+ case '.':
+ case '^':
+ case '_':
+ case '`':
+ case '|':
+ case '~':
+ return 1;
+ default:
+ return apr_isalnum(c);
+ }
+}
+
+static int ptoken_char(char c)
+{
+ switch (c) {
+ case '!':
+ case '#':
+ case '$':
+ case '&':
+ case '\'':
+ case '(':
+ case ')':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '/':
+ case ':':
+ case '<':
+ case '=':
+ case '>':
+ case '?':
+ case '@':
+ case '[':
+ case ']':
+ case '^':
+ case '_':
+ case '`':
+ case '{':
+ case '|':
+ case '}':
+ case '~':
+ return 1;
+ default:
+ return apr_isalnum(c);
+ }
+}
+
+static int skip_ws(link_ctx *ctx)
+{
+ char c;
+ while (ctx->i < ctx->slen
+ && (((c = ctx->s[ctx->i]) == ' ') || (c == '\t'))) {
+ ++ctx->i;
+ }
+ return (ctx->i < ctx->slen);
+}
+
+static int skip_nonws(link_ctx *ctx)
+{
+ char c;
+ while (ctx->i < ctx->slen
+ && (((c = ctx->s[ctx->i]) != ' ') && (c != '\t'))) {
+ ++ctx->i;
+ }
+ return (ctx->i < ctx->slen);
+}
+
+static unsigned int find_chr(link_ctx *ctx, char c, apr_size_t *pidx)
+{
+ apr_size_t j;
+ for (j = ctx->i; j < ctx->slen; ++j) {
+ if (ctx->s[j] == c) {
+ *pidx = j;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int read_chr(link_ctx *ctx, char c)
+{
+ if (ctx->i < ctx->slen && ctx->s[ctx->i] == c) {
+ ++ctx->i;
+ return 1;
+ }
+ return 0;
+}
+
+static int skip_qstring(link_ctx *ctx)
+{
+ if (skip_ws(ctx) && read_chr(ctx, '\"')) {
+ apr_size_t end;
+ if (find_chr(ctx, '\"', &end)) {
+ ctx->i = end + 1;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int skip_ptoken(link_ctx *ctx)
+{
+ if (skip_ws(ctx)) {
+ apr_size_t i;
+ for (i = ctx->i; i < ctx->slen && ptoken_char(ctx->s[i]); ++i) {
+ /* nop */
+ }
+ if (i > ctx->i) {
+ ctx->i = i;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+static int read_link(link_ctx *ctx)
+{
+ ctx->link_start = ctx->link_len = 0;
+ if (skip_ws(ctx) && read_chr(ctx, '<')) {
+ apr_size_t end;
+ if (find_chr(ctx, '>', &end)) {
+ ctx->link_start = ctx->i;
+ ctx->link_len = end - ctx->link_start;
+ ctx->i = end + 1;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int skip_pname(link_ctx *ctx)
+{
+ if (skip_ws(ctx)) {
+ apr_size_t i;
+ for (i = ctx->i; i < ctx->slen && attr_char(ctx->s[i]); ++i) {
+ /* nop */
+ }
+ if (i > ctx->i) {
+ ctx->i = i;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int skip_pvalue(link_ctx *ctx)
+{
+ if (skip_ws(ctx) && read_chr(ctx, '=')) {
+ ctx->pv_start = ctx->i;
+ if (skip_qstring(ctx) || skip_ptoken(ctx)) {
+ ctx->pv_len = ctx->i - ctx->pv_start;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int skip_param(link_ctx *ctx)
+{
+ if (skip_ws(ctx) && read_chr(ctx, ';')) {
+ ctx->pn_start = ctx->i;
+ ctx->pn_len = 0;
+ if (skip_pname(ctx)) {
+ ctx->pn_len = ctx->i - ctx->pn_start;
+ ctx->pv_len = 0;
+ skip_pvalue(ctx); /* value is optional */
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int pv_contains(link_ctx *ctx, const char *s)
+{
+ apr_size_t pvstart = ctx->pv_start;
+ apr_size_t pvlen = ctx->pv_len;
+
+ if (ctx->s[pvstart] == '\"' && pvlen > 1) {
+ ++pvstart;
+ pvlen -= 2;
+ }
+ if (pvlen > 0) {
+ apr_size_t slen = strlen(s);
+ link_ctx pvctx;
+ apr_size_t i;
+
+ memset(&pvctx, 0, sizeof(pvctx));
+ pvctx.s = ctx->s + pvstart;
+ pvctx.slen = pvlen;
+
+ for (i = 0; i < pvctx.slen; i = pvctx.i) {
+ skip_nonws(&pvctx);
+ if ((pvctx.i - i) == slen && !strncmp(s, pvctx.s + i, slen)) {
+ return 1;
+ }
+ skip_ws(&pvctx);
+ }
+ }
+ return 0;
+}
+
+/* RFC 5988 <https://tools.ietf.org/html/rfc5988#section-6.2.1>
+ Link = "Link" ":" #link-value
+ link-value = "<" URI-Reference ">" *( ";" link-param )
+ link-param = ( ( "rel" "=" relation-types )
+ | ( "anchor" "=" <"> URI-Reference <"> )
+ | ( "rev" "=" relation-types )
+ | ( "hreflang" "=" Language-Tag )
+ | ( "media" "=" ( MediaDesc | ( <"> MediaDesc <"> ) ) )
+ | ( "title" "=" quoted-string )
+ | ( "title*" "=" ext-value )
+ | ( "type" "=" ( media-type | quoted-mt ) )
+ | ( link-extension ) )
+ link-extension = ( parmname [ "=" ( ptoken | quoted-string ) ] )
+ | ( ext-name-star "=" ext-value )
+ ext-name-star = parmname "*" ; reserved for RFC2231-profiled
+ ; extensions. Whitespace NOT
+ ; allowed in between.
+ ptoken = 1*ptokenchar
+ ptokenchar = "!" | "#" | "$" | "%" | "&" | "'" | "("
+ | ")" | "*" | "+" | "-" | "." | "/" | DIGIT
+ | ":" | "<" | "=" | ">" | "?" | "@" | ALPHA
+ | "[" | "]" | "^" | "_" | "`" | "{" | "|"
+ | "}" | "~"
+ media-type = type-name "/" subtype-name
+ quoted-mt = <"> media-type <">
+ relation-types = relation-type
+ | <"> relation-type *( 1*SP relation-type ) <">
+ relation-type = reg-rel-type | ext-rel-type
+ reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" )
+ ext-rel-type = URI
+
+ and from <https://tools.ietf.org/html/rfc5987>
+ parmname = 1*attr-char
+ attr-char = ALPHA / DIGIT
+ / "!" / "#" / "$" / "&" / "+" / "-" / "."
+ / "^" / "_" / "`" / "|" / "~"
+ */
+
+typedef struct {
+ apr_pool_t *pool;
+ const char *relation;
+ const char *url;
+} find_ctx;
+
+static int find_url(void *baton, const char *key, const char *value)
+{
+ find_ctx *outer = baton;
+
+ if (!apr_strnatcasecmp("link", key)) {
+ link_ctx ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.s = value;
+ ctx.slen = strlen(value);
+
+ while (read_link(&ctx)) {
+ while (skip_param(&ctx)) {
+ if (ctx.pn_len == 3 && !strncmp("rel", ctx.s + ctx.pn_start, 3)
+ && pv_contains(&ctx, outer->relation)) {
+ /* this is the link relation we are looking for */
+ outer->url = apr_pstrndup(outer->pool, ctx.s + ctx.link_start, ctx.link_len);
+ return 0;
+ }
+ }
+ }
+ }
+ return 1;
+}
+
+const char *md_link_find_relation(const apr_table_t *headers,
+ apr_pool_t *pool, const char *relation)
+{
+ find_ctx ctx;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.pool = pool;
+ ctx.relation = relation;
+
+ apr_table_do(find_url, &ctx, headers, NULL);
+
+ return ctx.url;
+}
+
+const char *md_util_parse_ct(apr_pool_t *pool, const char *cth)
+{
+ char *type;
+ const char *p;
+ apr_size_t hlen;
+
+ if (!cth) return NULL;
+
+ for( p = cth; *p && *p != ' ' && *p != ';'; ++p)
+ ;
+ hlen = (apr_size_t)(p - cth);
+ type = apr_pcalloc( pool, hlen + 1 );
+ assert(type);
+ memcpy(type, cth, hlen);
+ type[hlen] = '\0';
+
+ return type;
+ /* Could parse and return parameters here, but we don't need any at present.
+ */
+}
diff --git a/modules/md/md_util.h b/modules/md/md_util.h
new file mode 100644
index 0000000..e430655
--- /dev/null
+++ b/modules/md/md_util.h
@@ -0,0 +1,253 @@
+/* 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_util_h
+#define mod_md_md_util_h
+
+#include <stdio.h>
+#include <apr_file_io.h>
+
+struct apr_array_header_t;
+struct apr_table_t;
+
+/**************************************************************************************************/
+/* pool utils */
+
+typedef apr_status_t md_util_action(void *baton, apr_pool_t *p, apr_pool_t *ptemp);
+typedef apr_status_t md_util_vaction(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap);
+
+apr_status_t md_util_pool_do(md_util_action *cb, void *baton, apr_pool_t *p);
+apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, ...);
+
+/**************************************************************************************************/
+/* data chunks */
+
+typedef void md_data_free_fn(void *data);
+
+typedef struct md_data_t md_data_t;
+struct md_data_t {
+ const char *data;
+ apr_size_t len;
+ md_data_free_fn *free_data;
+};
+
+/**
+ * Init the data to empty, overwriting any content.
+ */
+void md_data_null(md_data_t *d);
+
+/**
+ * Create a new md_data_t, providing `len` bytes allocated from pool `p`.
+ */
+md_data_t *md_data_pmake(apr_size_t len, apr_pool_t *p);
+/**
+ * Initialize md_data_t 'd', providing `len` bytes allocated from pool `p`.
+ */
+void md_data_pinit(md_data_t *d, apr_size_t len, apr_pool_t *p);
+/**
+ * Initialize md_data_t 'd', by borrowing 'len' bytes in `data` without copying.
+ * `d` will not take ownership.
+ */
+void md_data_init(md_data_t *d, const char *data, apr_size_t len);
+
+/**
+ * Initialize md_data_t 'd', by borrowing the NUL-terminated `str`.
+ * `d` will not take ownership.
+ */
+void md_data_init_str(md_data_t *d, const char *str);
+
+/**
+ * Free any present data and clear (NULL) it. Passing NULL is permitted.
+ */
+void md_data_clear(md_data_t *d);
+
+md_data_t *md_data_make_pcopy(apr_pool_t *p, const char *data, apr_size_t len);
+
+apr_status_t md_data_assign_copy(md_data_t *dest, const char *src, apr_size_t src_len);
+void md_data_assign_pcopy(md_data_t *dest, const char *src, apr_size_t src_len, apr_pool_t *p);
+
+apr_status_t md_data_to_hex(const char **phex, char separator,
+ 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 */
+char *md_util_str_tolower(char *s);
+
+/**
+ * Return != 0 iff array is either NULL or empty
+ */
+int md_array_is_empty(const struct apr_array_header_t *array);
+
+int md_array_str_index(const struct apr_array_header_t *array, const char *s,
+ int start, int case_sensitive);
+
+int md_array_str_eq(const struct apr_array_header_t *a1,
+ const struct apr_array_header_t *a2, int case_sensitive);
+
+struct apr_array_header_t *md_array_str_clone(apr_pool_t *p, struct apr_array_header_t *array);
+
+/**
+ * Create a new array with duplicates removed.
+ */
+struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src,
+ int case_sensitive);
+
+/**
+ * Create a new array with all occurrences of <exclude> removed.
+ */
+struct apr_array_header_t *md_array_str_remove(apr_pool_t *p, struct apr_array_header_t *src,
+ const char *exclude, int case_sensitive);
+
+int md_array_str_add_missing(struct apr_array_header_t *dest,
+ struct apr_array_header_t *src, int case_sensitive);
+
+/**************************************************************************************************/
+/* process execution */
+
+apr_status_t md_util_exec(apr_pool_t *p, const char *cmd, const char * const *argv,
+ struct apr_array_header_t *env, int *exit_code);
+
+/**************************************************************************************************/
+/* dns name check */
+
+/**
+ * Is a host/domain name using allowed characters. Not a wildcard.
+ * @param domain name to check
+ * @param need_fqdn iff != 0, check that domain contains '.'
+ * @return != 0 iff domain looks like a non-wildcard, legal DNS domain name.
+ */
+int md_dns_is_name(apr_pool_t *p, const char *domain, int need_fqdn);
+
+/**
+ * Check if the given domain is a valid wildcard DNS name, e.g. *.example.org
+ * @param domain name to check
+ * @return != 0 iff domain is a DNS wildcard.
+ */
+int md_dns_is_wildcard(apr_pool_t *p, const char *domain);
+
+/**
+ * Determine iff pattern matches domain, including case-ignore and wildcard domains.
+ * It is assumed that both names follow dns syntax.
+ * @return != 0 iff pattern matches domain
+ */
+int md_dns_matches(const char *pattern, const char *domain);
+
+/**
+ * Create a new array with the minimal set out of the given domain names that match all
+ * of them. If none of the domains is a wildcard, only duplicates are removed.
+ * If domains contain a wildcard, any name matching the wildcard will be removed.
+ */
+struct apr_array_header_t *md_dns_make_minimal(apr_pool_t *p,
+ struct apr_array_header_t *domains);
+
+/**
+ * Determine if the given domains cover the name, including wildcard matching.
+ * @return != 0 iff name is matched by list of domains
+ */
+int md_dns_domains_match(const apr_array_header_t *domains, const char *name);
+
+/**************************************************************************************************/
+/* file system related */
+
+struct apr_file_t;
+struct apr_finfo_t;
+
+apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode);
+
+apr_status_t md_util_fcreatex(struct apr_file_t **pf, const char *fn,
+ apr_fileperms_t perms, apr_pool_t *p);
+
+apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...);
+
+apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool);
+apr_status_t md_util_is_file(const char *path, apr_pool_t *pool);
+apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool);
+int md_file_exists(const char *fname, apr_pool_t *p);
+
+typedef apr_status_t md_util_file_cb(void *baton, struct apr_file_t *f, apr_pool_t *p);
+
+apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool_t *p,
+ md_util_file_cb *write, void *baton);
+
+/**
+ * Remove a file/directory and all files/directories contain up to max_level. If max_level == 0,
+ * only an empty directory or a file can be removed.
+ */
+apr_status_t md_util_rm_recursive(const char *fpath, apr_pool_t *p, int max_level);
+
+typedef apr_status_t md_util_fdo_cb(void *baton, apr_pool_t *p, apr_pool_t *ptemp,
+ const char *dir, const char *name,
+ apr_filetype_e ftype);
+
+apr_status_t md_util_files_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p,
+ const char *path, ...);
+
+/**
+ * Depth first traversal of directory tree starting at path.
+ */
+apr_status_t md_util_tree_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p,
+ const char *path, int follow_links);
+
+apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p);
+
+apr_status_t md_text_fread8k(const char **ptext, apr_pool_t *p, const char *fpath);
+apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t
+ perms, apr_pool_t *p, const char *text);
+apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms,
+ apr_pool_t *p, const char *text);
+
+/**************************************************************************************************/
+/* base64 url encodings */
+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);
+
+/**************************************************************************************************/
+/* http/url related */
+const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme);
+
+apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *s, const char **perr);
+apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr);
+
+const char *md_link_find_relation(const struct apr_table_t *headers,
+ apr_pool_t *pool, const char *relation);
+
+const char *md_util_parse_ct(apr_pool_t *pool, const char *cth);
+/**************************************************************************************************/
+/* retry logic */
+
+typedef apr_status_t md_util_try_fn(void *baton, int i);
+
+apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs,
+ apr_interval_time_t timeout, apr_interval_time_t start_delay,
+ apr_interval_time_t max_delay, int backoff);
+
+#endif /* md_util_h */
diff --git a/modules/md/md_version.h b/modules/md/md_version.h
new file mode 100644
index 0000000..a8f3ef2
--- /dev/null
+++ b/modules/md/md_version.h
@@ -0,0 +1,43 @@
+/* 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_version_h
+#define mod_md_md_version_h
+
+#undef PACKAGE_VERSION
+#undef PACKAGE_TARNAME
+#undef PACKAGE_STRING
+#undef PACKAGE_NAME
+#undef PACKAGE_BUGREPORT
+
+/**
+ * @macro
+ * Version number of the md module as c string
+ */
+#define MOD_MD_VERSION "2.4.21"
+
+/**
+ * @macro
+ * Numerical representation of the version number of the md module
+ * 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 0x020415
+
+#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory"
+#define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock"
+
+#endif /* mod_md_md_version_h */
diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c
new file mode 100644
index 0000000..32dea4f
--- /dev/null
+++ b/modules/md/mod_md.c
@@ -0,0 +1,1535 @@
+/* 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_strings.h>
+
+#include <mpm_common.h>
+#include <httpd.h>
+#include <http_core.h>
+#include <http_protocol.h>
+#include <http_request.h>
+#include <http_ssl.h>
+#include <http_log.h>
+#include <http_vhost.h>
+#include <ap_listen.h>
+
+#include "mod_status.h"
+
+#include "md.h"
+#include "md_curl.h"
+#include "md_crypt.h"
+#include "md_event.h"
+#include "md_http.h"
+#include "md_json.h"
+#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"
+#include "md_acme_authz.h"
+
+#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"
+
+static void md_hooks(apr_pool_t *pool);
+
+AP_DECLARE_MODULE(md) = {
+ STANDARD20_MODULE_STUFF,
+ NULL, /* func to create per dir config */
+ NULL, /* func to merge per dir config */
+ md_config_create_svr, /* func to create per server config */
+ md_config_merge_svr, /* func to merge per server config */
+ md_cmds, /* command handlers */
+ md_hooks,
+#if defined(AP_MODULE_FLAG_NONE)
+ AP_MODULE_FLAG_ALWAYS_MERGE
+#endif
+};
+
+/**************************************************************************************************/
+/* logging setup */
+
+static server_rec *log_server;
+
+static int log_is_level(void *baton, apr_pool_t *p, md_log_level_t level)
+{
+ (void)baton;
+ (void)p;
+ if (log_server) {
+ return APLOG_IS_LEVEL(log_server, (int)level);
+ }
+ return level <= MD_LOG_INFO;
+}
+
+#define LOG_BUF_LEN 16*1024
+
+static void log_print(const char *file, int line, md_log_level_t level,
+ apr_status_t rv, void *baton, apr_pool_t *p, const char *fmt, va_list ap)
+{
+ if (log_is_level(baton, p, level)) {
+ char buffer[LOG_BUF_LEN];
+
+ memset(buffer, 0, sizeof(buffer));
+ apr_vsnprintf(buffer, LOG_BUF_LEN-1, fmt, ap);
+ buffer[LOG_BUF_LEN-1] = '\0';
+
+ if (log_server) {
+ ap_log_error(file, line, APLOG_MODULE_INDEX, (int)level, rv, log_server, "%s", buffer);
+ }
+ else {
+ ap_log_perror(file, line, APLOG_MODULE_INDEX, (int)level, rv, p, "%s", buffer);
+ }
+ }
+}
+
+/**************************************************************************************************/
+/* mod_ssl interface */
+
+static void init_ssl(void)
+{
+ /* nop */
+}
+
+/**************************************************************************************************/
+/* lifecycle */
+
+static apr_status_t cleanup_setups(void *dummy)
+{
+ (void)dummy;
+ log_server = NULL;
+ return APR_SUCCESS;
+}
+
+static void init_setups(apr_pool_t *p, server_rec *base_server)
+{
+ log_server = base_server;
+ apr_pool_cleanup_register(p, NULL, cleanup_setups, apr_pool_cleanup_null);
+}
+
+/**************************************************************************************************/
+/* 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[] = {
+ { "renewing", apr_time_from_sec(MD_SECS_PER_HOUR) }, /* once per hour */
+ { "renewed", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */
+ { "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 (since_last.start > 0 && md_timeperiod_length(&since_last) < min_interim) {
+ /* not enough time has passed since we sent the last notification
+ * for this reason. */
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, APLOGNO(10267)
+ "%s: rate limiting notification about '%s'", job->mdomain, 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, NULL, &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, NULL, &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;
+}
+
+static apr_status_t on_event(const char *event, const char *mdomain, void *baton,
+ md_job_t *job, md_result_t *result, apr_pool_t *p)
+{
+ (void)mdomain;
+ return notify(job, event, result, p, baton);
+}
+
+/**************************************************************************************************/
+/* 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,
+ const char *fname, apr_filetype_e ftype,
+ apr_pool_t *p)
+{
+ server_rec *s = baton;
+ apr_status_t rv;
+
+ (void)store;
+ ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, s, "store event=%d on %s %s (group %d)",
+ ev, (ftype == APR_DIR)? "dir" : "file", fname, group);
+
+ /* Directories in group CHALLENGES, STAGING and OCSP are written to
+ * under a different user. Give her ownership.
+ */
+ if (ftype == APR_DIR) {
+ 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;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t check_group_dir(md_store_t *store, md_store_group_t group,
+ apr_pool_t *p, server_rec *s)
+{
+ const char *dir;
+ apr_status_t rv;
+
+ if (APR_SUCCESS == (rv = md_store_get_fname(&dir, store, group, NULL, NULL, p))
+ && APR_SUCCESS == (rv = apr_dir_make_recursive(dir, MD_FPROT_D_UALL_GREAD, p))) {
+ rv = store_file_ev(s, store, MD_S_FS_EV_CREATED, group, dir, APR_DIR, p);
+ }
+ return rv;
+}
+
+static apr_status_t setup_store(md_store_t **pstore, md_mod_conf_t *mc,
+ apr_pool_t *p, server_rec *s)
+{
+ const char *base_dir;
+ apr_status_t rv;
+
+ base_dir = ap_server_root_relative(p, mc->base_dir);
+
+ 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 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;
+ }
+
+leave:
+ return rv;
+}
+
+/**************************************************************************************************/
+/* post config handling */
+
+static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p)
+{
+ const char *contact;
+
+ if (!md->sc) {
+ md->sc = base_sc;
+ }
+
+ if (!md->ca_urls && md->sc->ca_urls) {
+ md->ca_urls = apr_array_copy(p, md->sc->ca_urls);
+ }
+ if (!md->ca_proto) {
+ md->ca_proto = md_config_gets(md->sc, MD_CONFIG_CA_PROTO);
+ }
+ if (!md->ca_agreement) {
+ md->ca_agreement = md_config_gets(md->sc, MD_CONFIG_CA_AGREEMENT);
+ }
+ contact = md_config_gets(md->sc, MD_CONFIG_CA_CONTACT);
+ if (md->contacts && md->contacts->nelts > 0) {
+ /* set explicitly */
+ }
+ else if (contact && contact[0]) {
+ apr_array_clear(md->contacts);
+ APR_ARRAY_PUSH(md->contacts, const char *) =
+ md_util_schemify(p, contact, "mailto");
+ }
+ else if( md->sc->s->server_admin && strcmp(DEFAULT_ADMIN, md->sc->s->server_admin)) {
+ apr_array_clear(md->contacts);
+ APR_ARRAY_PUSH(md->contacts, const char *) =
+ md_util_schemify(p, md->sc->s->server_admin, "mailto");
+ }
+ if (md->renew_mode == MD_RENEW_DEFAULT) {
+ md->renew_mode = md_config_geti(md->sc, MD_CONFIG_DRIVE_MODE);
+ }
+ if (!md->renew_window) md_config_get_timespan(&md->renew_window, md->sc, MD_CONFIG_RENEW_WINDOW);
+ if (!md->warn_window) md_config_get_timespan(&md->warn_window, md->sc, MD_CONFIG_WARN_WINDOW);
+ if (md->transitive < 0) {
+ md->transitive = md_config_geti(md->sc, MD_CONFIG_TRANSITIVE);
+ }
+ if (!md->ca_challenges && md->sc->ca_challenges) {
+ md->ca_challenges = apr_array_copy(p, md->sc->ca_challenges);
+ }
+ if (md_pkeys_spec_is_empty(md->pks)) {
+ md->pks = md->sc->pks;
+ }
+ if (md->require_https < 0) {
+ md->require_https = md_config_geti(md->sc, MD_CONFIG_REQUIRE_HTTPS);
+ }
+ if (!md->ca_eab_kid) {
+ md->ca_eab_kid = md->sc->ca_eab_kid;
+ md->ca_eab_hmac = md->sc->ca_eab_hmac;
+ }
+ 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,
+ 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 {
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10040)
+ "Virtual Host %s:%d matches Managed Domain '%s', but the "
+ "name/alias %s itself is not managed. A requested MD certificate "
+ "will not match ServerName.",
+ s->server_hostname, s->port, md->name, domain);
+ return APR_EINVAL;
+ }
+}
+
+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, 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, 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 uses_port(server_rec *s, int port)
+{
+ server_addr_rec *sa;
+ int match = 0;
+ 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 */
+ match = 1;
+ }
+ else {
+ /* uses other port/wildcard */
+ return 0;
+ }
+ }
+ return match;
+}
+
+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;
+
+ 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))) {
+ can_http = 1;
+ }
+ else if (sa->port == mc->local_443
+ && (!lr->protocol || !strncmp("http", lr->protocol, 4))) {
+ 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: 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_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;
+ server_rec *res = NULL;
+ request_rec r;
+ int i;
+ int check_port = 1;
+
+ sc = md_config_get(base_server);
+ mc = sc->mc;
+ memset(&r, 0, sizeof(r));
+
+ if (md->ca_challenges && md->ca_challenges->nelts > 0) {
+ /* skip the port check if "tls-alpn-01" is pre-configured */
+ check_port = !(md_array_str_index(md->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 0) >= 0);
+ }
+
+ if (check_port && !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 && check_port && 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)) {
+ if (check_port) {
+ return s;
+ }
+ else {
+ /* there may be multiple matching servers because we ignore the port.
+ if possible, choose a server that supports the acme-tls/1 protocol */
+ if (ap_is_allowed_protocol(NULL, NULL, s, PROTO_ACME_TLS_1)) {
+ return s;
+ }
+ res = s;
+ }
+ }
+ }
+ }
+ }
+ return res;
+}
+
+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;
+
+ /* 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. */
+ 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*);
+ 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;
+ }
+ if (!ap_is_allowed_protocol(NULL, NULL, s, PROTO_ACME_TLS_1)) {
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10169)
+ "%s: https server_rec for %s does not have protocol %s enabled",
+ md->name, domain, PROTO_ACME_TLS_1);
+ continue;
+ }
+ APR_ARRAY_PUSH(md->acme_tls_1_domains, const char*) = domain;
+ }
+}
+
+static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec *base_server,
+ apr_pool_t *p)
+{
+ server_rec *s;
+ request_rec r;
+ md_srv_conf_t *sc;
+ int i;
+ const char *domain, *uri;
+
+ sc = md_config_get(base_server);
+
+ /* Assign the MD to all server_rec configs that it matches. If there already
+ * is an assigned MD not equal this one, the configuration is in error.
+ */
+ memset(&r, 0, sizeof(r));
+ 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 */
+ continue;
+ }
+
+ r.server = s;
+ for (i = 0; i < md->domains->nelts; ++i) {
+ domain = APR_ARRAY_IDX(md->domains, i, const char*);
+
+ if (ap_matches_request_vhost(&r, domain, s->port)
+ || (md_dns_is_wildcard(p, domain) && md_dns_matches(domain, s->server_hostname))) {
+ /* 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) for domain %s, "
+ "has now %d MDs",
+ s->server_hostname, s->port, md->name, sc->name,
+ domain, (int)sc->assigned->nelts);
+
+ if (md->contacts && md->contacts->nelts > 0) {
+ /* set explicitly */
+ }
+ else if (sc->ca_contact && sc->ca_contact[0]) {
+ uri = md_util_schemify(p, sc->ca_contact, "mailto");
+ if (md_array_str_index(md->contacts, uri, 0, 0) < 0) {
+ APR_ARRAY_PUSH(md->contacts, const char *) = uri;
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044)
+ "%s: added contact %s", md->name, uri);
+ }
+ }
+ else 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) {
+ APR_ARRAY_PUSH(md->contacts, const char *) = uri;
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10237)
+ "%s: added contact %s", md->name, uri);
+ }
+ }
+ break;
+ }
+ }
+ }
+ return APR_SUCCESS;
+}
+
+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;
+ apr_status_t rv = APR_SUCCESS;
+
+ 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))) {
+ goto leave;
+ }
+ }
+leave:
+ return rv;
+}
+
+static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p,
+ server_rec *base_server, int log_level)
+{
+ md_srv_conf_t *base_conf;
+ md_t *md, *omd;
+ const char *domain;
+ md_timeslice_t *ts;
+ apr_status_t rv = APR_SUCCESS;
+ int i, j;
+
+ /* The global module configuration 'mc' keeps a list of all configured MDomains
+ * in the server. This list is collected during configuration processing and,
+ * in the post config phase, get updated from all merged server configurations
+ * before the server starts processing.
+ */
+ base_conf = md_config_get(base_server);
+ md_config_get_timespan(&ts, base_conf, MD_CONFIG_RENEW_WINDOW);
+ if (ts) md_reg_set_renew_window_default(mc->reg, ts);
+ md_config_get_timespan(&ts, base_conf, MD_CONFIG_WARN_WINDOW);
+ if (ts) md_reg_set_warn_window_default(mc->reg, ts);
+
+ /* Complete the properties of the MDs, now that we have the complete, merged
+ * server configurations.
+ */
+ for (i = 0; i < mc->mds->nelts; ++i) {
+ md = APR_ARRAY_IDX(mc->mds, i, md_t*);
+ merge_srv_config(md, base_conf, p);
+
+ /* Check that we have no overlap with the MDs already completed */
+ for (j = 0; j < i; ++j) {
+ omd = APR_ARRAY_IDX(mc->mds, j, md_t*);
+ if ((domain = md_common_name(md, omd)) != NULL) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10038)
+ "two Managed Domains have an overlap in domain '%s'"
+ ", first definition in %s(line %d), second in %s(line %d)",
+ domain, md->defn_name, md->defn_line_number,
+ omd->defn_name, omd->defn_line_number);
+ return APR_EINVAL;
+ }
+ }
+
+ if (md->cert_files && md->cert_files->nelts) {
+ if (!md->pkey_files || (md->cert_files->nelts != md->pkey_files->nelts)) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10170)
+ "The Managed Domain '%s' "
+ "needs one MDCertificateKeyFile for each MDCertificateFile.",
+ md->name);
+ return APR_EINVAL;
+ }
+ }
+ else if (md->pkey_files && md->pkey_files->nelts
+ && (!md->cert_files || !md->cert_files->nelts)) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10171)
+ "The Managed Domain '%s' "
+ "has MDCertificateKeyFile(s) but no MDCertificateFile.",
+ md->name);
+ return APR_EINVAL;
+ }
+
+ 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 "
+ "renew_window=%s, warn_window=%s",
+ md->name, md->ca_effective, md->ca_proto, md->ca_agreement, md->renew_mode,
+ md->renew_window? md_timeslice_format(md->renew_window, p) : "unset",
+ md->warn_window? md_timeslice_format(md->warn_window, p) : "unset");
+ }
+ }
+ return rv;
+}
+
+static apr_status_t check_invalid_duplicates(server_rec *base_server)
+{
+ server_rec *s;
+ md_srv_conf_t *sc;
+
+ ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, base_server,
+ "checking 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 int init_cert_watch_status(md_mod_conf_t *mc, apr_pool_t *p, apr_pool_t *ptemp, server_rec *s)
+{
+ md_t *md;
+ md_result_t *result;
+ 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);
+ for (i = 0; i < mc->mds->nelts; ++i) {
+ 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 "
+ "indicates an incomplete or inconsistent configuration. "
+ "Please check the log for warnings in this regard.");
+ continue;
+ }
+
+ if (md->renew_mode == MD_RENEW_AUTO
+ && md_array_str_index(mc->unused_names, md->name, 0, 0) >= 0) {
+ /* This MD is not used in any virtualhost, do not watch */
+ continue;
+ }
+
+ if (md_will_renew_cert(md)) {
+ /* make a test init to detect early errors. */
+ md_reg_test_init(mc->reg, md, mc->env, result, p);
+ if (APR_SUCCESS != result->status && result->detail) {
+ apr_hash_set(mc->init_errors, md->name, APR_HASH_KEY_STRING, apr_pstrdup(p, result->detail));
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10173)
+ "md[%s]: %s", md->name, result->detail);
+ }
+ }
+
+ md->watched = 1;
+ ++count;
+ }
+ return count;
+}
+
+static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog,
+ apr_pool_t *ptemp, server_rec *s)
+{
+ void *data = NULL;
+ const char *mod_md_init_key = "mod_md_init_counter";
+ md_srv_conf_t *sc;
+ md_mod_conf_t *mc;
+ apr_status_t rv = APR_SUCCESS;
+ int dry_run = 0, log_level = APLOG_DEBUG;
+ md_store_t *store;
+
+ apr_pool_userdata_get(&data, mod_md_init_key, s->process->pool);
+ if (data == NULL) {
+ /* At the first start, httpd makes a config check dry run. It
+ * runs all config hooks to check if it can. If so, it does
+ * this all again and starts serving requests.
+ *
+ * On a dry run, we therefore do all the cheap config things we
+ * need to do to find out if the settings are ok. More expensive
+ * things we delay to the real run.
+ */
+ dry_run = 1;
+ log_level = APLOG_TRACE1;
+ ap_log_error( APLOG_MARK, log_level, 0, s, APLOGNO(10070)
+ "initializing post config dry run");
+ apr_pool_userdata_set((const void *)1, mod_md_init_key,
+ apr_pool_cleanup_null, s->process->pool);
+ }
+ else {
+ ap_log_error( APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10071)
+ "mod_md (v%s), initializing...", MOD_MD_VERSION);
+ }
+
+ (void)plog;
+ init_setups(p, s);
+ md_log_set(log_is_level, log_print, NULL);
+
+ md_config_post_config(s, p);
+ sc = md_config_get(s);
+ mc = sc->mc;
+ mc->dry_run = dry_run;
+
+ md_event_init(p);
+ md_event_subscribe(on_event, mc);
+
+ rv = setup_store(&store, mc, p, s);
+ 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->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;
+ }
+
+ /* renew on 30% remaining /*/
+ rv = md_ocsp_reg_make(&mc->ocsp, p, store, mc->ocsp_renew_window,
+ AP_SERVER_BASEVERSION, mc->proxy_url,
+ mc->min_delay);
+ if (APR_SUCCESS != rv) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10196) "setup ocsp registry");
+ goto leave;
+ }
+
+ init_ssl();
+
+ /* How to bootstrap this module:
+ * 1. find out if we know if http: and/or https: requests will arrive
+ * 2. apply the now complete configuration settings 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.
+ * Basically all MD properties that are configured directly.
+ * WARNING: this may change the name of an MD. If an MD loses the first
+ * of its domain names, it first gets the new first one as name. The
+ * store will find the old settings and "recover" the previous name.
+ * 5. Load any staged data from previous driving.
+ * 6. on a dry run, this is all we do
+ * 7. Read back the MD properties that reflect the existence and aspect of
+ * credentials that are in the store (or missing there).
+ * Expiry times, MD state, etc.
+ * 8. Determine the list of MDs that need driving/supervision.
+ * 9. Cleanup any left-overs in registry/store that are no longer needed for
+ * the list of MDs as we know it now.
+ * 10. If this list is non-empty, setup a watchdog to run.
+ */
+ /*1*/
+ 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))) 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*/
+ md_reg_load_stagings(mc->reg, mc->mds, mc->env, p);
+leave:
+ md_reg_unlock_global(mc->reg, ptemp);
+ 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 (!sc || !sc->mc || sc->mc->dry_run) goto leave;
+ mc = sc->mc;
+
+ /*7*/
+ 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 *);
+
+ ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "md{%s}: auto_add", md->name);
+ if (APR_SUCCESS != (rv = auto_add_domains(md, s, p))) {
+ goto leave;
+ }
+ init_acme_tls_1_domains(md, s);
+ ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "md{%s}: check_usage", md->name);
+ if (APR_SUCCESS != (rv = check_usage(mc, md, s, p, ptemp))) {
+ goto leave;
+ }
+ ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "md{%s}: sync_finish", md->name);
+ 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 syncing to store", md->name);
+ goto leave;
+ }
+ }
+ /*8*/
+ ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "init_cert_watch");
+ watched = init_cert_watch_status(mc, p, ptemp, s);
+ /*9*/
+ ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "cleanup challenges");
+ md_reg_cleanup_challenges(mc->reg, p, ptemp, mc->mds);
+
+ /* From here on, the domains in the registry are readonly
+ * and only staging/challenges may be manipulated */
+ md_reg_freeze_domains(mc->reg, mc->mds);
+
+ if (watched) {
+ /*10*/
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10074)
+ "%d out of %d mds need watching", watched, mc->mds->nelts);
+
+ md_http_use_implementation(md_curl_get_impl(p));
+ rv = md_renew_start_watching(mc, s, p);
+ }
+ else {
+ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075) "no mds to supervise");
+ }
+
+ if (!mc->ocsp || md_ocsp_count(mc->ocsp) == 0) {
+ ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, s, "no ocsp to manage");
+ goto leave;
+ }
+
+ md_http_use_implementation(md_curl_get_impl(p));
+ rv = md_ocsp_start_watching(mc, s, p);
+
+leave:
+ ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "post_config done");
+ return rv;
+}
+
+/**************************************************************************************************/
+/* connection context */
+
+typedef struct {
+ const char *protocol;
+} md_conn_ctx;
+
+static const char *md_protocol_get(const conn_rec *c)
+{
+ md_conn_ctx *ctx;
+
+ ctx = (md_conn_ctx*)ap_get_module_config(c->conn_config, &md_module);
+ return ctx? ctx->protocol : NULL;
+}
+
+/**************************************************************************************************/
+/* ALPN handling */
+
+static int md_protocol_propose(conn_rec *c, request_rec *r,
+ server_rec *s,
+ const apr_array_header_t *offers,
+ apr_array_header_t *proposals)
+{
+ (void)s;
+ if (!r && offers && ap_ssl_conn_is_ssl(c)
+ && ap_array_str_contains(offers, PROTO_ACME_TLS_1)) {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
+ "proposing protocol '%s'", PROTO_ACME_TLS_1);
+ APR_ARRAY_PUSH(proposals, const char*) = PROTO_ACME_TLS_1;
+ return OK;
+ }
+ return DECLINED;
+}
+
+static int md_protocol_switch(conn_rec *c, request_rec *r, server_rec *s,
+ const char *protocol)
+{
+ md_conn_ctx *ctx;
+
+ (void)s;
+ if (!r && ap_ssl_conn_is_ssl(c) && !strcmp(PROTO_ACME_TLS_1, protocol)) {
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
+ "switching protocol '%s'", PROTO_ACME_TLS_1);
+ ctx = apr_pcalloc(c->pool, sizeof(*ctx));
+ ctx->protocol = PROTO_ACME_TLS_1;
+ ap_set_module_config(c->conn_config, &md_module, ctx);
+
+ c->keepalive = AP_CONN_CLOSE;
+ return OK;
+ }
+ return DECLINED;
+}
+
+
+/**************************************************************************************************/
+/* Access API to other httpd components */
+
+static void fallback_fnames(apr_pool_t *p, md_pkey_spec_t *kspec, char **keyfn, char **certfn )
+{
+ *keyfn = apr_pstrcat(p, "fallback-", md_pkey_filename(kspec, p), NULL);
+ *certfn = apr_pstrcat(p, "fallback-", md_chain_filename(kspec, p), NULL);
+}
+
+static apr_status_t make_fallback_cert(md_store_t *store, const md_t *md, md_pkey_spec_t *kspec,
+ server_rec *s, apr_pool_t *p, char *keyfn, char *crtfn)
+{
+ md_pkey_t *pkey;
+ md_cert_t *cert;
+ apr_status_t rv;
+
+ if (APR_SUCCESS != (rv = md_pkey_gen(&pkey, p, kspec))
+ || APR_SUCCESS != (rv = md_store_save(store, p, MD_SG_DOMAINS, md->name,
+ keyfn, MD_SV_PKEY, (void*)pkey, 0))
+ || APR_SUCCESS != (rv = md_cert_self_sign(&cert, "Apache Managed Domain Fallback",
+ md->domains, pkey, apr_time_from_sec(14 * MD_SECS_PER_DAY), p))
+ || APR_SUCCESS != (rv = md_store_save(store, p, MD_SG_DOMAINS, md->name,
+ crtfn, MD_SV_CERT, (void*)cert, 0))) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10174)
+ "%s: make fallback %s certificate", md->name, md_pkey_spec_name(kspec));
+ }
+ return rv;
+}
+
+static apr_status_t get_certificates(server_rec *s, apr_pool_t *p, int fallback,
+ apr_array_header_t **pcert_files,
+ apr_array_header_t **pkey_files)
+{
+ apr_status_t rv = APR_ENOENT;
+ md_srv_conf_t *sc;
+ md_reg_t *reg;
+ md_store_t *store;
+ const md_t *md;
+ apr_array_header_t *key_files, *chain_files;
+ const char *keyfile, *chainfile;
+ int i;
+
+ *pkey_files = *pcert_files = NULL;
+ key_files = apr_array_make(p, 5, sizeof(const char*));
+ chain_files = apr_array_make(p, 5, sizeof(const char*));
+
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10113)
+ "get_certificates called for vhost %s.", s->server_hostname);
+
+ sc = md_config_get(s);
+ if (!sc) {
+ ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, s,
+ "asked for certificate of server %s which has no md config",
+ s->server_hostname);
+ 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;
+ }
+ else if (sc->assigned->nelts != 1) {
+ if (!fallback) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10238)
+ "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*);
+
+ if (md->cert_files && md->cert_files->nelts) {
+ apr_array_cat(chain_files, md->cert_files);
+ apr_array_cat(key_files, md->pkey_files);
+ rv = APR_SUCCESS;
+ }
+ else {
+ md_pkey_spec_t *spec;
+
+ for (i = 0; i < md_cert_count(md); ++i) {
+ spec = md_pkeys_spec_get(md->pks, i);
+ rv = md_reg_get_cred_files(&keyfile, &chainfile, reg, MD_SG_DOMAINS, md, spec, p);
+ if (APR_SUCCESS == rv) {
+ APR_ARRAY_PUSH(key_files, const char*) = keyfile;
+ APR_ARRAY_PUSH(chain_files, const char*) = chainfile;
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ /* certificate for this pkey is not available, others might
+ * if pkeys have been added for a running mdomain.
+ * see issue #260 */
+ rv = APR_SUCCESS;
+ }
+ else if (!APR_STATUS_IS_ENOENT(rv)) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10110)
+ "retrieving credentials for MD %s (%s)",
+ md->name, md_pkey_spec_name(spec));
+ return rv;
+ }
+ }
+
+ if (md_array_is_empty(key_files)) {
+ if (fallback) {
+ /* Provide temporary, self-signed certificate as fallback, so that
+ * clients do not get obscure TLS handshake errors or will see a fallback
+ * virtual host that is not intended to be served here. */
+ char *kfn, *cfn;
+
+ store = md_reg_store_get(reg);
+ assert(store);
+
+ for (i = 0; i < md_cert_count(md); ++i) {
+ spec = md_pkeys_spec_get(md->pks, i);
+ fallback_fnames(p, spec, &kfn, &cfn);
+
+ md_store_get_fname(&keyfile, store, MD_SG_DOMAINS, md->name, kfn, p);
+ md_store_get_fname(&chainfile, store, MD_SG_DOMAINS, md->name, cfn, p);
+ if (!md_file_exists(keyfile, p) || !md_file_exists(chainfile, p)) {
+ if (APR_SUCCESS != (rv = make_fallback_cert(store, md, spec, s, p, kfn, cfn))) {
+ return rv;
+ }
+ }
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10116)
+ "%s: providing %s fallback certificate for server %s",
+ md->name, md_pkey_spec_name(spec), s->server_hostname);
+ APR_ARRAY_PUSH(key_files, const char*) = keyfile;
+ APR_ARRAY_PUSH(chain_files, const char*) = chainfile;
+ }
+ rv = APR_EAGAIN;
+ goto leave;
+ }
+ }
+ }
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10077)
+ "%s[state=%d]: providing certificates for server %s",
+ md->name, md->state, s->server_hostname);
+leave:
+ if (!md_array_is_empty(key_files) && !md_array_is_empty(chain_files)) {
+ *pkey_files = key_files;
+ *pcert_files = chain_files;
+ }
+ else if (APR_SUCCESS == rv) {
+ rv = APR_ENOENT;
+ }
+ return rv;
+}
+
+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)
+{
+ apr_array_header_t *md_cert_files;
+ apr_array_header_t *md_key_files;
+ apr_status_t rv;
+
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, "hook ssl_add_cert_files for %s",
+ s->server_hostname);
+ rv = get_certificates(s, p, 0, &md_cert_files, &md_key_files);
+ if (APR_SUCCESS == rv) {
+ if (!apr_is_empty_array(cert_files)) {
+ /* downgraded fromm WARNING to DEBUG, since installing separate certificates
+ * may be a valid use case. */
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10084)
+ "host '%s' is covered by a Managed Domain, but "
+ "certificate/key files are already configured "
+ "for it (most likely via SSLCertificateFile).",
+ s->server_hostname);
+ }
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s,
+ "host '%s' is covered by a Managed Domaina and "
+ "is being provided with %d key/certificate files.",
+ s->server_hostname, md_cert_files->nelts);
+ apr_array_cat(cert_files, md_cert_files);
+ apr_array_cat(key_files, md_key_files);
+ return DONE;
+ }
+ return DECLINED;
+}
+
+static int md_add_fallback_cert_files(server_rec *s, apr_pool_t *p,
+ apr_array_header_t *cert_files,
+ apr_array_header_t *key_files)
+{
+ apr_array_header_t *md_cert_files;
+ apr_array_header_t *md_key_files;
+ apr_status_t rv;
+
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, "hook ssl_add_fallback_cert_files for %s",
+ s->server_hostname);
+ rv = get_certificates(s, p, 1, &md_cert_files, &md_key_files);
+ if (APR_EAGAIN == rv) {
+ apr_array_cat(cert_files, md_cert_files);
+ apr_array_cat(key_files, md_key_files);
+ return DONE;
+ }
+ return DECLINED;
+}
+
+static int md_answer_challenge(conn_rec *c, const char *servername,
+ const char **pcert_pem, const char **pkey_pem)
+{
+ const char *protocol;
+ int hook_rv = DECLINED;
+ apr_status_t rv = APR_ENOENT;
+ md_srv_conf_t *sc;
+ md_store_t *store;
+ char *cert_name, *pkey_name;
+ const char *cert_pem, *key_pem;
+ int i;
+
+ if (!servername
+ || !(protocol = md_protocol_get(c))
+ || strcmp(PROTO_ACME_TLS_1, protocol)) {
+ goto cleanup;
+ }
+ sc = md_config_get(c->base_server);
+ if (!sc || !sc->mc->reg) goto cleanup;
+
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
+ "Answer challenge[tls-alpn-01] for %s", servername);
+ store = md_reg_store_get(sc->mc->reg);
+
+ for (i = 0; i < md_pkeys_spec_count( sc->pks ); i++) {
+ tls_alpn01_fnames(c->pool, md_pkeys_spec_get(sc->pks,i),
+ &pkey_name, &cert_name);
+
+ rv = md_store_load(store, MD_SG_CHALLENGES, servername, cert_name, MD_SV_TEXT,
+ (void**)&cert_pem, c->pool);
+ if (APR_STATUS_IS_ENOENT(rv)) continue;
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ rv = md_store_load(store, MD_SG_CHALLENGES, servername, pkey_name, MD_SV_TEXT,
+ (void**)&key_pem, c->pool);
+ if (APR_STATUS_IS_ENOENT(rv)) continue;
+ if (APR_SUCCESS != rv) goto cleanup;
+
+ ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c,
+ "Found challenge cert %s, key %s for %s",
+ cert_name, pkey_name, servername);
+ *pcert_pem = cert_pem;
+ *pkey_pem = key_pem;
+ hook_rv = OK;
+ break;
+ }
+
+ if (DECLINED == hook_rv) {
+ ap_log_cerror(APLOG_MARK, APLOG_INFO, rv, c, APLOGNO(10080)
+ "%s: unknown tls-alpn-01 challenge host", servername);
+ }
+
+cleanup:
+ return hook_rv;
+}
+
+
+/**************************************************************************************************/
+/* ACME 'http-01' challenge responses */
+
+#define WELL_KNOWN_PREFIX "/.well-known/"
+#define ACME_CHALLENGE_PREFIX WELL_KNOWN_PREFIX"acme-challenge/"
+
+static int md_http_challenge_pr(request_rec *r)
+{
+ apr_bucket_brigade *bb;
+ const md_srv_conf_t *sc;
+ const char *name, *data;
+ md_reg_t *reg;
+ const md_t *md;
+ apr_status_t rv;
+
+ if (r->parsed_uri.path
+ && !strncmp(ACME_CHALLENGE_PREFIX, r->parsed_uri.path, sizeof(ACME_CHALLENGE_PREFIX)-1)) {
+ sc = ap_get_module_config(r->server->module_config, &md_module);
+ if (sc && sc->mc) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
+ "access inside /.well-known/acme-challenge for %s%s",
+ r->hostname, r->parsed_uri.path);
+ md = md_get_by_domain(sc->mc->mds, r->hostname);
+ name = r->parsed_uri.path + sizeof(ACME_CHALLENGE_PREFIX)-1;
+ reg = sc && sc->mc? sc->mc->reg : NULL;
+
+ if (md && md->ca_challenges
+ && md_array_str_index(md->ca_challenges, MD_AUTHZ_CHA_HTTP_01, 0, 1) < 0) {
+ /* The MD this challenge is for does not allow http-01 challanges,
+ * we have to decline. See #279 for a setup example where this
+ * is necessary.
+ */
+ return DECLINED;
+ }
+
+ if (strlen(name) && !ap_strchr_c(name, '/') && reg) {
+ md_store_t *store = md_reg_store_get(reg);
+
+ rv = md_store_load(store, MD_SG_CHALLENGES, r->hostname,
+ MD_FN_HTTP01, MD_SV_TEXT, (void**)&data, r->pool);
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r,
+ "loading challenge for %s (%s)", r->hostname, r->uri);
+ if (APR_SUCCESS == rv) {
+ apr_size_t len = strlen(data);
+
+ if (r->method_number != M_GET) {
+ return HTTP_NOT_IMPLEMENTED;
+ }
+ /* A GET on a challenge resource for a hostname we are
+ * configured for. Let's send the content back */
+ r->status = HTTP_OK;
+ apr_table_setn(r->headers_out, "Content-Length", apr_ltoa(r->pool, (long)len));
+
+ bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+ apr_brigade_write(bb, NULL, NULL, data, len);
+ ap_pass_brigade(r->output_filters, bb);
+ apr_brigade_cleanup(bb);
+
+ return DONE;
+ }
+ else if (!md || md->renew_mode == MD_RENEW_MANUAL
+ || (md->cert_files && md->cert_files->nelts
+ && md->renew_mode == MD_RENEW_AUTO)) {
+ /* The request hostname is not for a domain - or at least not for
+ * a domain that we renew ourselves. We are not
+ * the sole authority here for /.well-known/acme-challenge (see PR62189).
+ * So, we decline to handle this and give others a chance to provide
+ * the answer.
+ */
+ return DECLINED;
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ return HTTP_NOT_FOUND;
+ }
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10081)
+ "loading challenge %s from store", name);
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+ }
+ }
+ return DECLINED;
+}
+
+/**************************************************************************************************/
+/* Require Https hook */
+
+static int md_require_https_maybe(request_rec *r)
+{
+ const md_srv_conf_t *sc;
+ apr_uri_t uri;
+ const char *s, *host;
+ const md_t *md;
+ int status;
+
+ /* 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 || !r->parsed_uri.path
+ || !strncmp(WELL_KNOWN_PREFIX, r->parsed_uri.path, sizeof(WELL_KNOWN_PREFIX)-1)) {
+ goto declined;
+ }
+
+ host = ap_get_server_name_for_url(r);
+ md = md_get_for_domain(r->server, host);
+ if (!md) goto declined;
+
+ if (ap_ssl_conn_is_ssl(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 {
+ /* 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;
+}
+
+/* Runs once per created child process. Perform any process
+ * related initialization here.
+ */
+static void md_child_init(apr_pool_t *pool, server_rec *s)
+{
+ (void)pool;
+ (void)s;
+}
+
+/* Install this module into the apache2 infrastructure.
+ */
+static void md_hooks(apr_pool_t *pool)
+{
+ static const char *const mod_ssl[] = { "mod_ssl.c", "mod_tls.c", NULL};
+ static const char *const mod_wd[] = { "mod_watchdog.c", NULL};
+
+ /* Leave the ssl initialization to mod_ssl or friends. */
+ md_acme_init(pool, AP_SERVER_BASEVERSION, 0);
+
+ ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks");
+
+ /* Run once after configuration is set, before mod_ssl.
+ * 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, mod_wd, APR_HOOK_LAST);
+
+ /* Run once after a child process has been created.
+ */
+ ap_hook_child_init(md_child_init, NULL, mod_ssl, APR_HOOK_MIDDLE);
+
+ /* answer challenges *very* early, before any configured authentication may strike */
+ ap_hook_post_read_request(md_require_https_maybe, mod_ssl, NULL, APR_HOOK_MIDDLE);
+ ap_hook_post_read_request(md_http_challenge_pr, NULL, NULL, APR_HOOK_MIDDLE);
+
+ ap_hook_protocol_propose(md_protocol_propose, NULL, NULL, APR_HOOK_MIDDLE);
+ ap_hook_protocol_switch(md_protocol_switch, NULL, NULL, APR_HOOK_MIDDLE);
+ ap_hook_protocol_get(md_protocol_get, NULL, NULL, APR_HOOK_MIDDLE);
+
+ /* 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_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);
+
+ ap_hook_ssl_answer_challenge(md_answer_challenge, NULL, NULL, APR_HOOK_MIDDLE);
+ ap_hook_ssl_add_cert_files(md_add_cert_files, NULL, NULL, APR_HOOK_MIDDLE);
+ ap_hook_ssl_add_fallback_cert_files(md_add_fallback_cert_files, NULL, NULL, APR_HOOK_MIDDLE);
+
+#if AP_MODULE_MAGIC_AT_LEAST(20120211, 105)
+ ap_hook_ssl_ocsp_prime_hook(md_ocsp_prime_status, NULL, NULL, APR_HOOK_MIDDLE);
+ ap_hook_ssl_ocsp_get_resp_hook(md_ocsp_provide_status, NULL, NULL, APR_HOOK_MIDDLE);
+#else
+#error "This version of mod_md requires Apache httpd 2.4.48 or newer."
+#endif /* AP_MODULE_MAGIC_AT_LEAST() */
+}
+
diff --git a/modules/md/mod_md.dep b/modules/md/mod_md.dep
new file mode 100644
index 0000000..0cbd691
--- /dev/null
+++ b/modules/md/mod_md.dep
@@ -0,0 +1,5 @@
+# Microsoft Developer Studio Generated Dependency File, included by mod_md.mak
+
+..\..\build\win32\httpd.rc : \
+ "..\..\include\ap_release.h"\
+
diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp
new file mode 100644
index 0000000..d99fb1c
--- /dev/null
+++ b/modules/md/mod_md.dsp
@@ -0,0 +1,223 @@
+# Microsoft Developer Studio Project File - Name="mod_md" - Package Owner=<4>
+# Microsoft Developer Studio Generated Build File, Format Version 6.00
+# ** DO NOT EDIT **
+
+# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102
+
+CFG=mod_md - Win32 Release
+!MESSAGE This is not a valid makefile. To build this project using NMAKE,
+!MESSAGE use the Export Makefile command and run
+!MESSAGE
+!MESSAGE NMAKE /f "mod_md.mak".
+!MESSAGE
+!MESSAGE You can specify a configuration when running NMAKE
+!MESSAGE by defining the macro CFG on the command line. For example:
+!MESSAGE
+!MESSAGE NMAKE /f "mod_md.mak" CFG="mod_md - Win32 Release"
+!MESSAGE
+!MESSAGE Possible choices for configuration are:
+!MESSAGE
+!MESSAGE "mod_md - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE "mod_md - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE
+
+# Begin Project
+# PROP AllowPerConfigDependencies 0
+# PROP Scc_ProjName ""
+# PROP Scc_LocalPath ""
+CPP=cl.exe
+MTL=midl.exe
+RSC=rc.exe
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 0
+# PROP BASE Output_Dir "Release"
+# PROP BASE Intermediate_Dir "Release"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 0
+# PROP Output_Dir "Release"
+# PROP Intermediate_Dir "Release"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /MD /W3 /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c
+# ADD CPP /nologo /MD /W3 /O2 /Oy- /Zi /I "../../server/mpm/winnt" "/I ../ssl" /I "../../include" /I "../generators" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/openssl/inc32" /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Release\mod_md_src" /FD /c
+# ADD BASE MTL /nologo /D "NDEBUG" /win32
+# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32
+# ADD BASE RSC /l 0x409 /d "NDEBUG"
+# ADD RSC /l 0x409 /fo"Release/mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d "BIN_NAME=mod_md.so" /d "LONG_NAME=Letsencrypt module for Apache"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib /nologo /subsystem:windows /dll /out:".\Release\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+# ADD LINK32 kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib ssleay32.lib libeay32.lib jansson.lib libcurl.lib /libpath:"../../srclib/apr/Release" /libpath:"../../srclib/apr-util/Release" /libpath:"../../Release/" /libpath:"../../srclib/openssl/out32dll" /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Release\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so /opt:ref
+# Begin Special Build Tool
+TargetPath=.\Release\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2
+# End Special Build Tool
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 1
+# PROP BASE Output_Dir "Debug"
+# PROP BASE Intermediate_Dir "Debug"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 1
+# PROP Output_Dir "Debug"
+# PROP Intermediate_Dir "Debug"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /MDd /W3 /EHsc /Zi /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c
+# ADD CPP /nologo /MDd /W3 /EHsc /Zi /Od /I "../ssl" /I "../../include" /I "../../srclib/apr/include" /I "../generators" /I "../../srclib/apr-util/include" /I "../../srclib/openssl/inc32" /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /src" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Debug\mod_md_src" /FD /c
+# ADD BASE MTL /nologo /D "_DEBUG" /win32
+# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32
+# ADD BASE RSC /l 0x409 /d "_DEBUG"
+# ADD RSC /l 0x409 /fo"Debug/mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d "BIN_NAME=mod_md.so" /d "LONG_NAME=md_module for Apache"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Debug\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+# ADD LINK32 kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib ssleay32.lib libeay32.lib jansson_d.lib libcurl_debug.lib /nologo /subsystem:windows /dll /libpath:"../../srclib/openssl/out32dll" /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /incremental:no /debug /out:".\Debug\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+# Begin Special Build Tool
+TargetPath=.\Debug\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2
+# End Special Build Tool
+
+!ENDIF
+
+# Begin Target
+
+# Name "mod_md - Win32 Release"
+# Name "mod_md - Win32 Debug"
+# Begin Source File
+
+SOURCE=./mod_md.c
+# End Source File
+# Begin Source File
+
+SOURCE=./mod_md_config.c
+# End Source File
+# Begin Source File
+
+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
+
+SOURCE=./mod_md_status.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_acct.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_authz.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_drive.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_order.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acmev2_drive.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_core.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_crypt.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_curl.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_http.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_event.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_json.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_jws.c
+# End Source File
+# Begin Source File
+
+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
+
+SOURCE=./md_result.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_status.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_store.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_store_fs.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_tailscale.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_time.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_util.c
+# End Source File
+# Begin Source File
+
+SOURCE=..\..\build\win32\httpd.rc
+# End Source File
+# End Target
+# End Project
diff --git a/modules/md/mod_md.h b/modules/md/mod_md.h
new file mode 100644
index 0000000..805737d
--- /dev/null
+++ b/modules/md/mod_md.h
@@ -0,0 +1,20 @@
+/* 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_mod_md_h
+#define mod_md_mod_md_h
+
+#endif /* mod_md_mod_md_h */
diff --git a/modules/md/mod_md.mak b/modules/md/mod_md.mak
new file mode 100644
index 0000000..9779e6b
--- /dev/null
+++ b/modules/md/mod_md.mak
@@ -0,0 +1,618 @@
+# Microsoft Developer Studio Generated NMAKE File, Based on mod_md.dsp
+!IF "$(CFG)" == ""
+CFG=mod_md - Win32 Release
+!MESSAGE No configuration specified. Defaulting to mod_md - Win32 Release.
+!ENDIF
+
+!IF "$(CFG)" != "mod_md - Win32 Release" && "$(CFG)" != "mod_md - Win32 Debug"
+!MESSAGE Invalid configuration "$(CFG)" specified.
+!MESSAGE You can specify a configuration when running NMAKE
+!MESSAGE by defining the macro CFG on the command line. For example:
+!MESSAGE
+!MESSAGE NMAKE /f "mod_md.mak" CFG="mod_md - Win32 Release"
+!MESSAGE
+!MESSAGE Possible choices for configuration are:
+!MESSAGE
+!MESSAGE "mod_md - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE "mod_md - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE
+!ERROR An invalid configuration is specified.
+!ENDIF
+
+!IF "$(OS)" == "Windows_NT"
+NULL=
+!ELSE
+NULL=nul
+!ENDIF
+
+!IF "$(_HAVE_OSSL110)" == "1"
+SSLCRP=libcrypto
+SSLLIB=libssl
+SSLINC=/I ../../srclib/openssl/include
+SSLBIN=/libpath:../../srclib/openssl
+!ELSE
+SSLCRP=libeay32
+SSLLIB=ssleay32
+SSLINC=/I ../../srclib/openssl/inc32
+SSLBIN=/libpath:../../srclib/openssl/out32dll
+!ENDIF
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+OUTDIR=.\Release
+INTDIR=.\Release
+# Begin Custom Macros
+OutDir=.\Release
+# End Custom Macros
+
+!IF "$(RECURSE)" == "0"
+
+ALL : "$(OUTDIR)\mod_md.so"
+
+!ELSE
+
+ALL : "libhttpd - Win32 Release" "libaprutil - Win32 Release" "libapr - Win32 Release" "$(OUTDIR)\mod_md.so"
+
+!ENDIF
+
+!IF "$(RECURSE)" == "1"
+CLEAN :"libapr - Win32 ReleaseCLEAN" "libaprutil - Win32 ReleaseCLEAN" "libhttpd - Win32 ReleaseCLEAN"
+!ELSE
+CLEAN :
+!ENDIF
+ -@erase "$(INTDIR)\md_acme.obj"
+ -@erase "$(INTDIR)\md_acme_acct.obj"
+ -@erase "$(INTDIR)\md_acme_authz.obj"
+ -@erase "$(INTDIR)\md_acme_drive.obj"
+ -@erase "$(INTDIR)\md_acme_order.obj"
+ -@erase "$(INTDIR)\md_acmev2_drive.obj"
+ -@erase "$(INTDIR)\md_core.obj"
+ -@erase "$(INTDIR)\md_crypt.obj"
+ -@erase "$(INTDIR)\md_curl.obj"
+ -@erase "$(INTDIR)\md_event.obj"
+ -@erase "$(INTDIR)\md_http.obj"
+ -@erase "$(INTDIR)\md_json.obj"
+ -@erase "$(INTDIR)\md_jws.obj"
+ -@erase "$(INTDIR)\md_log.obj"
+ -@erase "$(INTDIR)\md_ocsp.obj"
+ -@erase "$(INTDIR)\md_reg.obj"
+ -@erase "$(INTDIR)\md_result.obj"
+ -@erase "$(INTDIR)\md_status.obj"
+ -@erase "$(INTDIR)\md_store.obj"
+ -@erase "$(INTDIR)\md_store_fs.obj"
+ -@erase "$(INTDIR)\md_tailscale.obj"
+ -@erase "$(INTDIR)\md_time.obj"
+ -@erase "$(INTDIR)\md_util.obj"
+ -@erase "$(INTDIR)\mod_md.obj"
+ -@erase "$(INTDIR)\mod_md.res"
+ -@erase "$(INTDIR)\mod_md_config.obj"
+ -@erase "$(INTDIR)\mod_md_drive.obj"
+ -@erase "$(INTDIR)\mod_md_status.obj"
+ -@erase "$(INTDIR)\mod_md_ocsp.obj"
+ -@erase "$(INTDIR)\mod_md_os.obj"
+ -@erase "$(INTDIR)\mod_md_src.idb"
+ -@erase "$(INTDIR)\mod_md_src.pdb"
+ -@erase "$(OUTDIR)\mod_md.exp"
+ -@erase "$(OUTDIR)\mod_md.lib"
+ -@erase "$(OUTDIR)\mod_md.pdb"
+ -@erase "$(OUTDIR)\mod_md.so"
+
+"$(OUTDIR)" :
+ if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"
+
+CPP=cl.exe
+CPP_PROJ=/nologo /MD /W3 /Zi /O2 /Oy- /I "../../server/mpm/winnt" /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" $(SSLINC) /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../ssl" /I "../core" /I "../generators" /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D ssize_t=long /Fo"$(INTDIR)\\" /Fd"$(INTDIR)\mod_md_src" /FD /I " ../ssl" /c
+
+.c{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.c{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+MTL=midl.exe
+MTL_PROJ=/nologo /D "NDEBUG" /mktyplib203 /win32
+RSC=rc.exe
+RSC_PROJ=/l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d BIN_NAME=mod_md.so /d LONG_NAME=Letsencrypt module for Apache
+BSC32=bscmake.exe
+BSC32_FLAGS=/nologo /o"$(OUTDIR)\mod_md.bsc"
+BSC32_SBRS= \
+
+LINK32=link.exe
+LINK32_FLAGS=kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib $(SSLCRP).lib $(SSLLIB).lib jansson.lib libcurl.lib /nologo /subsystem:windows /dll /incremental:no /pdb:"$(OUTDIR)\mod_md.pdb" /debug /out:"$(OUTDIR)\mod_md.so" /implib:"$(OUTDIR)\mod_md.lib" /libpath:"../../srclib/apr/Release" /libpath:"../../srclib/apr-util/Release" /libpath:"../../Release/" $(SSLBIN) /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so /opt:ref
+LINK32_OBJS= \
+ "$(INTDIR)\mod_md.obj" \
+ "$(INTDIR)\mod_md_config.obj" \
+ "$(INTDIR)\mod_md_drive.obj" \
+ "$(INTDIR)\mod_md_ocsp.obj" \
+ "$(INTDIR)\mod_md_os.obj" \
+ "$(INTDIR)\mod_md_status.obj" \
+ "$(INTDIR)\md_core.obj" \
+ "$(INTDIR)\md_crypt.obj" \
+ "$(INTDIR)\md_curl.obj" \
+ "$(INTDIR)\md_event.obj" \
+ "$(INTDIR)\md_http.obj" \
+ "$(INTDIR)\md_json.obj" \
+ "$(INTDIR)\md_jws.obj" \
+ "$(INTDIR)\md_log.obj" \
+ "$(INTDIR)\md_ocsp.obj" \
+ "$(INTDIR)\md_reg.obj" \
+ "$(INTDIR)\md_result.obj" \
+ "$(INTDIR)\md_status.obj" \
+ "$(INTDIR)\md_store.obj" \
+ "$(INTDIR)\md_store_fs.obj" \
+ "$(INTDIR)\md_tailscale.obj" \
+ "$(INTDIR)\md_time.obj" \
+ "$(INTDIR)\md_util.obj" \
+ "$(INTDIR)\md_acme.obj" \
+ "$(INTDIR)\md_acme_acct.obj" \
+ "$(INTDIR)\md_acme_authz.obj" \
+ "$(INTDIR)\md_acme_drive.obj" \
+ "$(INTDIR)\md_acme_order.obj" \
+ "$(INTDIR)\md_acmev2_drive.obj" \
+ "$(INTDIR)\mod_md.res" \
+ "..\..\srclib\apr\Release\libapr-1.lib" \
+ "..\..\srclib\apr-util\Release\libaprutil-1.lib" \
+ "..\..\Release\libhttpd.lib"
+
+"$(OUTDIR)\mod_md.so" : "$(OUTDIR)" $(DEF_FILE) $(LINK32_OBJS)
+ $(LINK32) @<<
+ $(LINK32_FLAGS) $(LINK32_OBJS)
+<<
+
+TargetPath=.\Release\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+DS_POSTBUILD_DEP=$(INTDIR)\postbld.dep
+
+ALL : $(DS_POSTBUILD_DEP)
+
+# Begin Custom Macros
+OutDir=.\Release
+# End Custom Macros
+
+$(DS_POSTBUILD_DEP) : "libhttpd - Win32 Release" "libaprutil - Win32 Release" "libapr - Win32 Release" "$(OUTDIR)\mod_md.so"
+ if exist .\Release\mod_md.so.manifest mt.exe -manifest .\Release\mod_md.so.manifest -outputresource:.\Release\mod_md.so;2
+ echo Helper for Post-build step > "$(DS_POSTBUILD_DEP)"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+OUTDIR=.\Debug
+INTDIR=.\Debug
+# Begin Custom Macros
+OutDir=.\Debug
+# End Custom Macros
+
+!IF "$(RECURSE)" == "0"
+
+ALL : "$(OUTDIR)\mod_md.so"
+
+!ELSE
+
+ALL : "libhttpd - Win32 Debug" "libaprutil - Win32 Debug" "libapr - Win32 Debug" "$(OUTDIR)\mod_md.so"
+
+!ENDIF
+
+!IF "$(RECURSE)" == "1"
+CLEAN :"libapr - Win32 DebugCLEAN" "libaprutil - Win32 DebugCLEAN" "libhttpd - Win32 DebugCLEAN"
+!ELSE
+CLEAN :
+!ENDIF
+ -@erase "$(INTDIR)\md_acme.obj"
+ -@erase "$(INTDIR)\md_acme_acct.obj"
+ -@erase "$(INTDIR)\md_acme_authz.obj"
+ -@erase "$(INTDIR)\md_acme_drive.obj"
+ -@erase "$(INTDIR)\md_acme_order.obj"
+ -@erase "$(INTDIR)\md_acmev2_drive.obj"
+ -@erase "$(INTDIR)\md_core.obj"
+ -@erase "$(INTDIR)\md_crypt.obj"
+ -@erase "$(INTDIR)\md_curl.obj"
+ -@erase "$(INTDIR)\md_event.obj"
+ -@erase "$(INTDIR)\md_http.obj"
+ -@erase "$(INTDIR)\md_json.obj"
+ -@erase "$(INTDIR)\md_jws.obj"
+ -@erase "$(INTDIR)\md_log.obj"
+ -@erase "$(INTDIR)\md_ocsp.obj"
+ -@erase "$(INTDIR)\md_reg.obj"
+ -@erase "$(INTDIR)\md_result.obj"
+ -@erase "$(INTDIR)\md_status.obj"
+ -@erase "$(INTDIR)\md_store.obj"
+ -@erase "$(INTDIR)\md_store_fs.obj"
+ -@erase "$(INTDIR)\md_tailscale.obj"
+ -@erase "$(INTDIR)\md_time.obj"
+ -@erase "$(INTDIR)\md_util.obj"
+ -@erase "$(INTDIR)\mod_md.obj"
+ -@erase "$(INTDIR)\mod_md.res"
+ -@erase "$(INTDIR)\mod_md_config.obj"
+ -@erase "$(INTDIR)\mod_md_drive.obj"
+ -@erase "$(INTDIR)\mod_md_status.obj"
+ -@erase "$(INTDIR)\mod_md_ocsp.obj"
+ -@erase "$(INTDIR)\mod_md_os.obj"
+ -@erase "$(INTDIR)\mod_md_src.idb"
+ -@erase "$(INTDIR)\mod_md_src.pdb"
+ -@erase "$(OUTDIR)\mod_md.exp"
+ -@erase "$(OUTDIR)\mod_md.lib"
+ -@erase "$(OUTDIR)\mod_md.pdb"
+ -@erase "$(OUTDIR)\mod_md.so"
+
+"$(OUTDIR)" :
+ if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"
+
+CPP=cl.exe
+CPP_PROJ=/nologo /MDd /W3 /Zi /Od /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" $(SSLINC) /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /I "../generators" /I "../ssl" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D ssize_t=long /Fo"$(INTDIR)\\" /Fd"$(INTDIR)\mod_md_src" /FD /EHsc /c
+
+.c{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.c{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+MTL=midl.exe
+MTL_PROJ=/nologo /D "_DEBUG" /mktyplib203 /win32
+RSC=rc.exe
+RSC_PROJ=/l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d BIN_NAME=mod_md.so /d LONG_NAME=http2_module for Apache
+BSC32=bscmake.exe
+BSC32_FLAGS=/nologo /o"$(OUTDIR)\mod_md.bsc"
+BSC32_SBRS= \
+
+LINK32=link.exe
+LINK32_FLAGS=kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib $(SSLCRP).lib $(SSLLIB).lib jansson_d.lib libcurl_debug.lib /nologo /subsystem:windows /dll /incremental:no /pdb:"$(OUTDIR)\mod_md.pdb" /debug /out:"$(OUTDIR)\mod_md.so" /implib:"$(OUTDIR)\mod_md.lib" $(SSLBIN) /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+LINK32_OBJS= \
+ "$(INTDIR)\mod_md.obj" \
+ "$(INTDIR)\mod_md_config.obj" \
+ "$(INTDIR)\mod_md_drive.obj" \
+ "$(INTDIR)\mod_md_ocsp.obj" \
+ "$(INTDIR)\mod_md_os.obj" \
+ "$(INTDIR)\mod_md_status.obj" \
+ "$(INTDIR)\md_core.obj" \
+ "$(INTDIR)\md_crypt.obj" \
+ "$(INTDIR)\md_curl.obj" \
+ "$(INTDIR)\md_event.obj" \
+ "$(INTDIR)\md_http.obj" \
+ "$(INTDIR)\md_json.obj" \
+ "$(INTDIR)\md_jws.obj" \
+ "$(INTDIR)\md_log.obj" \
+ "$(INTDIR)\md_ocsp.obj" \
+ "$(INTDIR)\md_reg.obj" \
+ "$(INTDIR)\md_result.obj" \
+ "$(INTDIR)\md_status.obj" \
+ "$(INTDIR)\md_store.obj" \
+ "$(INTDIR)\md_store_fs.obj" \
+ "$(INTDIR)\md_tailscale.obj" \
+ "$(INTDIR)\md_time.obj" \
+ "$(INTDIR)\md_util.obj" \
+ "$(INTDIR)\md_acme.obj" \
+ "$(INTDIR)\md_acme_acct.obj" \
+ "$(INTDIR)\md_acme_authz.obj" \
+ "$(INTDIR)\md_acme_drive.obj" \
+ "$(INTDIR)\md_acme_order.obj" \
+ "$(INTDIR)\md_acmev2_drive.obj" \
+ "$(INTDIR)\mod_md.res" \
+ "..\..\srclib\apr\Debug\libapr-1.lib" \
+ "..\..\srclib\apr-util\Debug\libaprutil-1.lib" \
+ "..\..\Debug\libhttpd.lib"
+
+"$(OUTDIR)\mod_md.so" : "$(OUTDIR)" $(DEF_FILE) $(LINK32_OBJS)
+ $(LINK32) @<<
+ $(LINK32_FLAGS) $(LINK32_OBJS)
+<<
+
+TargetPath=.\Debug\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+DS_POSTBUILD_DEP=$(INTDIR)\postbld.dep
+
+ALL : $(DS_POSTBUILD_DEP)
+
+# Begin Custom Macros
+OutDir=.\Debug
+# End Custom Macros
+
+$(DS_POSTBUILD_DEP) : "libhttpd - Win32 Debug" "libaprutil - Win32 Debug" "libapr - Win32 Debug" "$(OUTDIR)\mod_md.so"
+ if exist .\Debug\mod_md.so.manifest mt.exe -manifest .\Debug\mod_md.so.manifest -outputresource:.\Debug\mod_md.so;2
+ echo Helper for Post-build step > "$(DS_POSTBUILD_DEP)"
+
+!ENDIF
+
+
+!IF "$(NO_EXTERNAL_DEPS)" != "1"
+!IF EXISTS("mod_md.dep")
+!INCLUDE "mod_md.dep"
+!ELSE
+!MESSAGE Warning: cannot find "mod_md.dep"
+!ENDIF
+!ENDIF
+
+
+!IF "$(CFG)" == "mod_md - Win32 Release" || "$(CFG)" == "mod_md - Win32 Debug"
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+"libapr - Win32 Release" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Release"
+ cd "..\..\modules\md"
+
+"libapr - Win32 ReleaseCLEAN" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Release" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+"libapr - Win32 Debug" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Debug"
+ cd "..\..\modules\md"
+
+"libapr - Win32 DebugCLEAN" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Debug" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ENDIF
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+"libaprutil - Win32 Release" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Release"
+ cd "..\..\modules\md"
+
+"libaprutil - Win32 ReleaseCLEAN" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Release" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+"libaprutil - Win32 Debug" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Debug"
+ cd "..\..\modules\md"
+
+"libaprutil - Win32 DebugCLEAN" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Debug" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ENDIF
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+"libhttpd - Win32 Release" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Release"
+ cd ".\modules\md"
+
+"libhttpd - Win32 ReleaseCLEAN" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Release" RECURSE=1 CLEAN
+ cd ".\modules\md"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+"libhttpd - Win32 Debug" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Debug"
+ cd ".\modules\md"
+
+"libhttpd - Win32 DebugCLEAN" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Debug" RECURSE=1 CLEAN
+ cd ".\modules\md"
+
+!ENDIF
+
+SOURCE=..\..\build\win32\httpd.rc
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+
+"$(INTDIR)\mod_md.res" : $(SOURCE) "$(INTDIR)"
+ $(RSC) /l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /i "../../build\win32" /d "NDEBUG" /d BIN_NAME="mod_md.so" /d LONG_NAME="md_module for Apache" $(SOURCE)
+
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+
+"$(INTDIR)\mod_md.res" : $(SOURCE) "$(INTDIR)"
+ $(RSC) /l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /i "../../build\win32" /d "_DEBUG" /d BIN_NAME="mod_md.so" /d LONG_NAME="md_module for Apache" $(SOURCE)
+
+
+!ENDIF
+
+SOURCE=./md_acme.c
+
+"$(INTDIR)\md_acme.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_acct.c
+
+"$(INTDIR)\md_acme_acct.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_authz.c
+
+"$(INTDIR)\md_acme_authz.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_drive.c
+
+"$(INTDIR)\md_acme_drive.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_order.c
+
+"$(INTDIR)\md_acme_order.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acmev2_drive.c
+
+"$(INTDIR)\md_acmev2_drive.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_core.c
+
+"$(INTDIR)\md_core.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_crypt.c
+
+"$(INTDIR)\md_crypt.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_curl.c
+
+"$(INTDIR)\md_curl.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_event.c
+
+"$(INTDIR)\md_event.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_http.c
+
+"$(INTDIR)\md_http.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_json.c
+
+"$(INTDIR)\md_json.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_jws.c
+
+"$(INTDIR)\md_jws.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_log.c
+
+"$(INTDIR)\md_log.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_ocsp.c
+
+"$(INTDIR)\md_ocsp.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_reg.c
+
+"$(INTDIR)\md_reg.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_result.c
+
+"$(INTDIR)\md_result.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_status.c
+
+"$(INTDIR)\md_status.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_store.c
+
+"$(INTDIR)\md_store.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_store_fs.c
+
+"$(INTDIR)\md_store_fs.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_tailscale.c
+
+"$(INTDIR)\md_tailscale.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_time.c
+
+"$(INTDIR)\md_time.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_util.c
+
+"$(INTDIR)\md_util.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md.c
+
+"$(INTDIR)\mod_md.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_config.c
+
+"$(INTDIR)\mod_md_config.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_drive.c
+
+"$(INTDIR)\mod_md_drive.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_ocsp.c
+
+"$(INTDIR)\mod_md_ocsp.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_os.c
+
+"$(INTDIR)\mod_md_os.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_status.c
+
+"$(INTDIR)\mod_md_status.obj" : $(SOURCE) "$(INTDIR)"
+
+
+!ENDIF
+
diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c
new file mode 100644
index 0000000..e117b16
--- /dev/null
+++ b/modules/md/mod_md_config.c
@@ -0,0 +1,1388 @@
+/* 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_lib.h>
+#include <apr_strings.h>
+
+#include <httpd.h>
+#include <http_core.h>
+#include <http_config.h>
+#include <http_log.h>
+#include <http_vhost.h>
+
+#include "md.h"
+#include "md_acme.h"
+#include "md_crypt.h"
+#include "md_log.h"
+#include "md_json.h"
+#include "md_util.h"
+#include "mod_md_private.h"
+#include "mod_md_config.h"
+
+#define MD_CMD_MD_SECTION "<MDomainSet"
+#define MD_CMD_MD2_SECTION "<MDomain"
+
+#define DEF_VAL (-1)
+
+#ifndef MD_DEFAULT_BASE_DIR
+#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 dirm by default state-dir-relative */
+#else
+ MD_DEFAULT_BASE_DIR,
+#endif
+ NULL, /* proxy url for outgoing http */
+ NULL, /* md_reg_t */
+ NULL, /* md_ocsp_reg_t */
+ 80, /* local http: port */
+ 443, /* local https: port */
+ -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, /* init errors hash */
+ NULL, /* notify cmd */
+ NULL, /* message cmd */
+ NULL, /* env table */
+ 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 */
+ 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 = {
+ MD_TIME_LIFE_NORM,
+ MD_TIME_RENEW_WINDOW_DEF,
+};
+static md_timeslice_t def_warn_window = {
+ MD_TIME_LIFE_NORM,
+ MD_TIME_WARN_WINDOW_DEF,
+};
+
+/* Default server specific setting */
+static md_srv_conf_t defconf = {
+ "default", /* name */
+ NULL, /* server_rec */
+ &defmc, /* mc */
+ 1, /* transitive */
+ MD_REQUIRE_OFF, /* require https */
+ MD_RENEW_AUTO, /* renew mode */
+ 0, /* must staple */
+ NULL, /* pkey spec */
+ &def_renew_window, /* renew window */
+ &def_warn_window, /* warn window */
+ NULL, /* ca urls */
+ NULL, /* ca contact (email) */
+ MD_PROTO_ACME, /* ca protocol */
+ NULL, /* ca agreemnent */
+ NULL, /* ca challenges array */
+ NULL, /* ca eab kid */
+ NULL, /* ca eab hmac */
+ 0, /* stapling */
+ 1, /* staple others */
+ NULL, /* dns01_cmd */
+ 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;
+
+static apr_status_t cleanup_mod_config(void *dummy)
+{
+ (void)dummy;
+ mod_md_config = NULL;
+ return APR_SUCCESS;
+}
+
+static md_mod_conf_t *md_mod_conf_get(apr_pool_t *pool, int create)
+{
+ if (mod_md_config) {
+ return mod_md_config; /* reused for lifetime of the pool */
+ }
+
+ if (create) {
+ mod_md_config = apr_pcalloc(pool, sizeof(*mod_md_config));
+ 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->env = apr_table_make(pool, 10);
+ mod_md_config->init_errors = apr_hash_make(pool);
+
+ apr_pool_cleanup_register(pool, NULL, cleanup_mod_config, apr_pool_cleanup_null);
+ }
+
+ return mod_md_config;
+}
+
+#define CONF_S_NAME(s) (s && s->server_hostname? s->server_hostname : "default")
+
+static void srv_conf_props_clear(md_srv_conf_t *sc)
+{
+ sc->transitive = DEF_VAL;
+ sc->require_https = MD_REQUIRE_UNSET;
+ sc->renew_mode = DEF_VAL;
+ sc->must_staple = DEF_VAL;
+ sc->pks = NULL;
+ sc->renew_window = NULL;
+ sc->warn_window = NULL;
+ sc->ca_urls = NULL;
+ sc->ca_contact = NULL;
+ sc->ca_proto = NULL;
+ sc->ca_agreement = NULL;
+ sc->ca_challenges = NULL;
+ sc->ca_eab_kid = NULL;
+ sc->ca_eab_hmac = NULL;
+ sc->stapling = DEF_VAL;
+ sc->staple_others = DEF_VAL;
+ sc->dns01_cmd = NULL;
+}
+
+static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from)
+{
+ to->transitive = from->transitive;
+ to->require_https = from->require_https;
+ to->renew_mode = from->renew_mode;
+ to->must_staple = from->must_staple;
+ to->pks = from->pks;
+ to->warn_window = from->warn_window;
+ to->renew_window = from->renew_window;
+ to->ca_urls = from->ca_urls;
+ to->ca_contact = from->ca_contact;
+ to->ca_proto = from->ca_proto;
+ to->ca_agreement = from->ca_agreement;
+ to->ca_challenges = from->ca_challenges;
+ to->ca_eab_kid = from->ca_eab_kid;
+ to->ca_eab_hmac = from->ca_eab_hmac;
+ to->stapling = from->stapling;
+ to->staple_others = from->staple_others;
+ to->dns01_cmd = from->dns01_cmd;
+}
+
+static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t *p)
+{
+ if (from->require_https != MD_REQUIRE_UNSET) md->require_https = from->require_https;
+ if (from->transitive != DEF_VAL) md->transitive = from->transitive;
+ if (from->renew_mode != DEF_VAL) md->renew_mode = from->renew_mode;
+ if (from->must_staple != DEF_VAL) md->must_staple = from->must_staple;
+ if (from->pks) md->pks = md_pkeys_spec_clone(p, from->pks);
+ if (from->renew_window) md->renew_window = from->renew_window;
+ if (from->warn_window) md->warn_window = from->warn_window;
+ if (from->ca_urls) md->ca_urls = apr_array_copy(p, from->ca_urls);
+ if (from->ca_proto) md->ca_proto = from->ca_proto;
+ if (from->ca_agreement) md->ca_agreement = from->ca_agreement;
+ if (from->ca_contact) {
+ apr_array_clear(md->contacts);
+ APR_ARRAY_PUSH(md->contacts, const char *) =
+ md_util_schemify(p, from->ca_contact, "mailto");
+ }
+ if (from->ca_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges);
+ if (from->ca_eab_kid) md->ca_eab_kid = from->ca_eab_kid;
+ if (from->ca_eab_hmac) md->ca_eab_hmac = from->ca_eab_hmac;
+ if (from->stapling != DEF_VAL) md->stapling = from->stapling;
+ if (from->dns01_cmd) md->dns01_cmd = from->dns01_cmd;
+}
+
+void *md_config_create_svr(apr_pool_t *pool, server_rec *s)
+{
+ md_srv_conf_t *conf = (md_srv_conf_t *)apr_pcalloc(pool, sizeof(md_srv_conf_t));
+
+ conf->name = apr_pstrcat(pool, "srv[", CONF_S_NAME(s), "]", NULL);
+ conf->s = s;
+ conf->mc = md_mod_conf_get(pool, 1);
+
+ srv_conf_props_clear(conf);
+
+ return conf;
+}
+
+static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv)
+{
+ md_srv_conf_t *base = (md_srv_conf_t *)basev;
+ md_srv_conf_t *add = (md_srv_conf_t *)addv;
+ md_srv_conf_t *nsc;
+ char *name = apr_pstrcat(pool, "[", CONF_S_NAME(add->s), ", ", CONF_S_NAME(base->s), "]", NULL);
+
+ 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->transitive = (add->transitive != DEF_VAL)? add->transitive : base->transitive;
+ nsc->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https;
+ nsc->renew_mode = (add->renew_mode != DEF_VAL)? add->renew_mode : base->renew_mode;
+ nsc->must_staple = (add->must_staple != DEF_VAL)? add->must_staple : base->must_staple;
+ nsc->pks = (!md_pkeys_spec_is_empty(add->pks))? add->pks : base->pks;
+ nsc->renew_window = add->renew_window? add->renew_window : base->renew_window;
+ nsc->warn_window = add->warn_window? add->warn_window : base->warn_window;
+
+ nsc->ca_urls = add->ca_urls? apr_array_copy(pool, add->ca_urls)
+ : (base->ca_urls? apr_array_copy(pool, base->ca_urls) : NULL);
+ nsc->ca_contact = add->ca_contact? add->ca_contact : base->ca_contact;
+ nsc->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto;
+ 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->ca_eab_kid = add->ca_eab_kid? add->ca_eab_kid : base->ca_eab_kid;
+ nsc->ca_eab_hmac = add->ca_eab_hmac? add->ca_eab_hmac : base->ca_eab_hmac;
+ 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->dns01_cmd = (add->dns01_cmd)? add->dns01_cmd : base->dns01_cmd;
+ nsc->current = NULL;
+
+ return nsc;
+}
+
+void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv)
+{
+ return md_config_merge(pool, basev, addv);
+}
+
+static int inside_section(cmd_parms *cmd, const char *section) {
+ ap_directive_t *d;
+ for (d = cmd->directive->parent; d; d = d->parent) {
+ if (!ap_cstr_casecmp(d->directive, section)) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int inside_md_section(cmd_parms *cmd) {
+ return (inside_section(cmd, MD_CMD_MD_SECTION) || inside_section(cmd, MD_CMD_MD2_SECTION));
+}
+
+static const char *md_section_check(cmd_parms *cmd) {
+ if (!inside_md_section(cmd)) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name, " is only valid inside a '",
+ MD_CMD_MD_SECTION, "' context, not here", NULL);
+ }
+ return NULL;
+}
+
+#define MD_LOC_GLOBAL (0x01)
+#define MD_LOC_MD (0x02)
+#define MD_LOC_ELSE (0x04)
+#define MD_LOC_ALL (0x07)
+#define MD_LOC_NOT_MD (0x102)
+
+static const char *md_conf_check_location(cmd_parms *cmd, int flags)
+{
+ if (MD_LOC_GLOBAL == flags) {
+ return ap_check_cmd_context(cmd, GLOBAL_ONLY);
+ }
+ if (MD_LOC_NOT_MD == flags && inside_md_section(cmd)) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name, " is not allowed inside an '",
+ MD_CMD_MD_SECTION, "' context", NULL);
+ }
+ if (MD_LOC_MD == flags) {
+ return md_section_check(cmd);
+ }
+ else if ((MD_LOC_MD & flags) && inside_md_section(cmd)) {
+ return NULL;
+ }
+ return ap_check_cmd_context(cmd, NOT_IN_DIRECTORY|NOT_IN_LOCATION);
+}
+
+static const char *set_on_off(int *pvalue, const char *s, apr_pool_t *p)
+{
+ if (!apr_strnatcasecmp("off", s)) {
+ *pvalue = 0;
+ }
+ else if (!apr_strnatcasecmp("on", s)) {
+ *pvalue = 1;
+ }
+ else {
+ return apr_pstrcat(p, "unknown '", s,
+ "', supported parameter values are 'on' and 'off'", NULL);
+ }
+ return NULL;
+}
+
+
+static void add_domain_name(apr_array_header_t *domains, const char *name, apr_pool_t *p)
+{
+ if (md_array_str_index(domains, name, 0, 0) < 0) {
+ APR_ARRAY_PUSH(domains, char *) = md_util_str_tolower(apr_pstrdup(p, name));
+ }
+}
+
+static const char *set_transitive(int *ptransitive, const char *value)
+{
+ if (!apr_strnatcasecmp("auto", value)) {
+ *ptransitive = 1;
+ return NULL;
+ }
+ else if (!apr_strnatcasecmp("manual", value)) {
+ *ptransitive = 0;
+ return NULL;
+ }
+ return "unknown value, use \"auto|manual\"";
+}
+
+static const char *md_config_sec_start(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+ md_srv_conf_t *sc;
+ md_srv_conf_t save;
+ const char *endp;
+ const char *err, *name;
+ apr_array_header_t *domains;
+ md_t *md;
+ int transitive = -1;
+
+ (void)mconfig;
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+
+ sc = md_config_get(cmd->server);
+ endp = ap_strrchr_c(arg, '>');
+ if (endp == NULL) {
+ return MD_CMD_MD_SECTION "> directive missing closing '>'";
+ }
+
+ arg = apr_pstrndup(cmd->pool, arg, (apr_size_t)(endp-arg));
+ if (!arg || !*arg) {
+ return MD_CMD_MD_SECTION " > section must specify a unique domain name";
+ }
+
+ name = ap_getword_conf(cmd->pool, &arg);
+ domains = apr_array_make(cmd->pool, 5, sizeof(const char *));
+ add_domain_name(domains, name, cmd->pool);
+ while (*arg != '\0') {
+ name = ap_getword_conf(cmd->pool, &arg);
+ if (NULL != set_transitive(&transitive, name)) {
+ add_domain_name(domains, name, cmd->pool);
+ }
+ }
+
+ if (domains->nelts == 0) {
+ return "needs at least one domain name";
+ }
+
+ md = md_create(cmd->pool, domains);
+ if (transitive >= 0) {
+ md->transitive = transitive;
+ }
+
+ /* Save the current settings in this srv_conf and apply+restore at the
+ * end of this section */
+ memcpy(&save, sc, sizeof(save));
+ srv_conf_props_clear(sc);
+ sc->current = md;
+
+ if (NULL == (err = ap_walk_config(cmd->directive->first_child, cmd, cmd->context))) {
+ srv_conf_props_apply(md, sc, cmd->pool);
+ APR_ARRAY_PUSH(sc->mc->mds, const md_t *) = md;
+ }
+
+ sc->current = NULL;
+ srv_conf_props_copy(sc, &save);
+
+ return err;
+}
+
+static const char *md_config_sec_add_members(cmd_parms *cmd, void *dc,
+ int argc, char *const argv[])
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+ int i;
+
+ (void)dc;
+ if (NULL != (err = md_section_check(cmd))) {
+ if (argc == 1) {
+ /* only these values are allowed outside a section */
+ return set_transitive(&sc->transitive, argv[0]);
+ }
+ return err;
+ }
+
+ assert(sc->current);
+ for (i = 0; i < argc; ++i) {
+ if (NULL != set_transitive(&sc->transitive, argv[i])) {
+ add_domain_name(sc->current->domains, argv[i], cmd->pool);
+ }
+ }
+ return NULL;
+}
+
+static const char *md_config_set_names(cmd_parms *cmd, void *dc,
+ int argc, char *const argv[])
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ apr_array_header_t *domains = apr_array_make(cmd->pool, 5, sizeof(const char *));
+ const char *err;
+ md_t *md;
+ int i, transitive = -1;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+
+ for (i = 0; i < argc; ++i) {
+ if (NULL != set_transitive(&transitive, argv[i])) {
+ add_domain_name(domains, argv[i], cmd->pool);
+ }
+ }
+
+ if (domains->nelts == 0) {
+ return "needs at least one domain name";
+ }
+ md = md_create(cmd->pool, domains);
+
+ if (transitive >= 0) {
+ md->transitive = transitive;
+ }
+
+ if (cmd->config_file) {
+ md->defn_name = cmd->config_file->name;
+ md->defn_line_number = cmd->config_file->line_number;
+ }
+
+ APR_ARRAY_PUSH(sc->mc->mds, md_t *) = md;
+
+ return NULL;
+}
+
+static const char *md_config_set_ca(cmd_parms *cmd, void *dc,
+ int argc, char *const argv[])
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err, *url;
+ int i;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ if (!sc->ca_urls) {
+ sc->ca_urls = apr_array_make(cmd->pool, 3, sizeof(const char *));
+ }
+ else {
+ apr_array_clear(sc->ca_urls);
+ }
+ for (i = 0; i < argc; ++i) {
+ if (APR_SUCCESS != md_get_ca_url_from_name(&url, cmd->pool, argv[i])) {
+ return url;
+ }
+ APR_ARRAY_PUSH(sc->ca_urls, const char *) = url;
+ }
+ return NULL;
+}
+
+static const char *md_config_set_contact(cmd_parms *cmd, void *dc, const char *value)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ sc->ca_contact = value;
+ return NULL;
+}
+
+static const char *md_config_set_ca_proto(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 = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ config->ca_proto = value;
+ return NULL;
+}
+
+static const char *md_config_set_agreement(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 = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ config->ca_agreement = value;
+ return NULL;
+}
+
+static const char *md_config_set_renew_mode(cmd_parms *cmd, void *dc, const char *value)
+{
+ md_srv_conf_t *config = md_config_get(cmd->server);
+ const char *err;
+ md_renew_mode_t renew_mode;
+
+ (void)dc;
+ if (!apr_strnatcasecmp("auto", value) || !apr_strnatcasecmp("automatic", value)) {
+ renew_mode = MD_RENEW_AUTO;
+ }
+ else if (!apr_strnatcasecmp("always", value)) {
+ renew_mode = MD_RENEW_ALWAYS;
+ }
+ else if (!apr_strnatcasecmp("manual", value) || !apr_strnatcasecmp("stick", value)) {
+ renew_mode = MD_RENEW_MANUAL;
+ }
+ else {
+ return apr_pstrcat(cmd->pool, "unknown MDDriveMode ", value, NULL);
+ }
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ config->renew_mode = renew_mode;
+ return NULL;
+}
+
+static const char *md_config_set_must_staple(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 = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ 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 ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ 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 = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ 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);
+ const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD);
+
+ (void)dc;
+ if (err) return err;
+ return set_on_off(&config->mc->manage_base_server, value, cmd->pool);
+}
+
+static const char *md_config_set_min_delay(cmd_parms *cmd, void *dc, const char *value)
+{
+ md_srv_conf_t *config = md_config_get(cmd->server);
+ const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD);
+ apr_time_t delay;
+
+ (void)dc;
+ if (err) return err;
+ if (md_duration_parse(&delay, value, "s") != APR_SUCCESS) {
+ return "unrecognized duration format";
+ }
+ config->mc->min_delay = delay;
+ return NULL;
+}
+
+static const char *md_config_set_retry_failover(cmd_parms *cmd, void *dc, const char *value)
+{
+ md_srv_conf_t *config = md_config_get(cmd->server);
+ const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD);
+ int retry_failover;
+
+ (void)dc;
+ if (err) return err;
+ retry_failover = atoi(value);
+ if (retry_failover <= 0) {
+ return "invalid argument, must be a number > 0";
+ }
+ config->mc->retry_failover = retry_failover;
+ 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);
+ const char *err;
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ (void)dc;
+ if (!apr_strnatcasecmp("off", value)) {
+ config->require_https = MD_REQUIRE_OFF;
+ }
+ else if (!apr_strnatcasecmp(MD_KEY_TEMPORARY, value)) {
+ config->require_https = MD_REQUIRE_TEMPORARY;
+ }
+ else if (!apr_strnatcasecmp(MD_KEY_PERMANENT, value)) {
+ config->require_https = MD_REQUIRE_PERMANENT;
+ }
+ else {
+ return apr_pstrcat(cmd->pool, "unknown '", value,
+ "', supported parameter values are 'temporary' and 'permanent'", NULL);
+ }
+ return NULL;
+}
+
+static const char *md_config_set_renew_window(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 = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ err = md_timeslice_parse(&config->renew_window, cmd->pool, value, MD_TIME_LIFE_NORM);
+ if (!err && config->renew_window->norm
+ && (config->renew_window->len >= config->renew_window->norm)) {
+ err = "a length of 100% or more is not allowed.";
+ }
+ if (err) return apr_psprintf(cmd->pool, "MDRenewWindow %s", err);
+ return NULL;
+}
+
+static const char *md_config_set_warn_window(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 = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ err = md_timeslice_parse(&config->warn_window, cmd->pool, value, MD_TIME_LIFE_NORM);
+ if (!err && config->warn_window->norm
+ && (config->warn_window->len >= config->warn_window->norm)) {
+ err = "a length of 100% or more is not allowed.";
+ }
+ if (err) return apr_psprintf(cmd->pool, "MDWarnWindow %s", err);
+ return NULL;
+}
+
+static const char *md_config_set_proxy(cmd_parms *cmd, void *arg, const char *value)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+ md_util_abs_http_uri_check(cmd->pool, value, &err);
+ if (err) {
+ return err;
+ }
+ sc->mc->proxy_url = value;
+ (void)arg;
+ return NULL;
+}
+
+static const char *md_config_set_store_dir(cmd_parms *cmd, void *arg, const char *value)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+ sc->mc->base_dir = value;
+ (void)arg;
+ return NULL;
+}
+
+static const char *set_port_map(md_mod_conf_t *mc, const char *value)
+{
+ int net_port, local_port;
+ const char *endp;
+
+ if (!strncmp("http:", value, sizeof("http:") - 1)) {
+ net_port = 80; endp = value + sizeof("http") - 1;
+ }
+ else if (!strncmp("https:", value, sizeof("https:") - 1)) {
+ net_port = 443; endp = value + sizeof("https") - 1;
+ }
+ else {
+ net_port = (int)apr_strtoi64(value, (char**)&endp, 10);
+ if (errno) {
+ return "unable to parse first port number";
+ }
+ }
+ if (!endp || *endp != ':') {
+ return "no ':' after first port number";
+ }
+ ++endp;
+ if (*endp == '-') {
+ local_port = 0;
+ }
+ else {
+ local_port = (int)apr_strtoi64(endp, (char**)&endp, 10);
+ if (errno) {
+ return "unable to parse second port number";
+ }
+ if (local_port <= 0 || local_port > 65535) {
+ return "invalid number for port map, must be in ]0,65535]";
+ }
+ }
+ switch (net_port) {
+ case 80:
+ mc->local_80 = local_port;
+ break;
+ case 443:
+ mc->local_443 = local_port;
+ break;
+ default:
+ return "mapped port number must be 80 or 443";
+ }
+ return NULL;
+}
+
+static const char *md_config_set_port_map(cmd_parms *cmd, void *arg,
+ const char *v1, const char *v2)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ (void)arg;
+ if (!(err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ err = set_port_map(sc->mc, v1);
+ }
+ if (!err && v2) {
+ err = set_port_map(sc->mc, v2);
+ }
+ return err;
+}
+
+static const char *md_config_set_cha_tyes(cmd_parms *cmd, void *dc,
+ int argc, char *const argv[])
+{
+ md_srv_conf_t *config = md_config_get(cmd->server);
+ apr_array_header_t **pcha, *ca_challenges;
+ const char *err;
+ int i;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ pcha = &config->ca_challenges;
+
+ ca_challenges = *pcha;
+ if (ca_challenges) {
+ apr_array_clear(ca_challenges);
+ }
+ else {
+ *pcha = ca_challenges = apr_array_make(cmd->pool, 5, sizeof(const char *));
+ }
+ for (i = 0; i < argc; ++i) {
+ APR_ARRAY_PUSH(ca_challenges, const char *) = argv[i];
+ }
+
+ return NULL;
+}
+
+static const char *md_config_set_pkeys(cmd_parms *cmd, void *dc,
+ int argc, char *const argv[])
+{
+ md_srv_conf_t *config = md_config_get(cmd->server);
+ const char *err, *ptype;
+ apr_int64_t bits;
+ int i;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ if (argc <= 0) {
+ return "needs to specify the private key type";
+ }
+
+ config->pks = md_pkeys_spec_make(cmd->pool);
+ for (i = 0; i < argc; ++i) {
+ ptype = argv[i];
+ if (!apr_strnatcasecmp("Default", ptype)) {
+ if (argc > 1) {
+ return "'Default' allows no other parameter";
+ }
+ md_pkeys_spec_add_default(config->pks);
+ }
+ else if (strlen(ptype) > 3
+ && (ptype[0] == 'R' || ptype[0] == 'r')
+ && (ptype[1] == 'S' || ptype[1] == 's')
+ && (ptype[2] == 'A' || ptype[2] == 'a')
+ && isdigit(ptype[3])) {
+ bits = (int)apr_atoi64(ptype+3);
+ if (bits < MD_PKEY_RSA_BITS_MIN) {
+ return apr_psprintf(cmd->pool,
+ "must be %d or higher in order to be considered safe.",
+ MD_PKEY_RSA_BITS_MIN);
+ }
+ if (bits >= INT_MAX) {
+ return apr_psprintf(cmd->pool, "is too large for an RSA key length.");
+ }
+ if (md_pkeys_spec_contains_rsa(config->pks)) {
+ return "two keys of type 'RSA' are not possible.";
+ }
+ md_pkeys_spec_add_rsa(config->pks, (unsigned int)bits);
+ }
+ else if (!apr_strnatcasecmp("RSA", ptype)) {
+ if (i+1 >= argc || !isdigit(argv[i+1][0])) {
+ bits = MD_PKEY_RSA_BITS_DEF;
+ }
+ else {
+ ++i;
+ bits = (int)apr_atoi64(argv[i]);
+ if (bits < MD_PKEY_RSA_BITS_MIN) {
+ return apr_psprintf(cmd->pool,
+ "must be %d or higher in order to be considered safe.",
+ MD_PKEY_RSA_BITS_MIN);
+ }
+ if (bits >= INT_MAX) {
+ return apr_psprintf(cmd->pool, "is too large for an RSA key length.");
+ }
+ }
+ if (md_pkeys_spec_contains_rsa(config->pks)) {
+ return "two keys of type 'RSA' are not possible.";
+ }
+ md_pkeys_spec_add_rsa(config->pks, (unsigned int)bits);
+ }
+ else {
+ if (md_pkeys_spec_contains_ec(config->pks, argv[i])) {
+ return apr_psprintf(cmd->pool, "two keys of type '%s' are not possible.", argv[i]);
+ }
+ md_pkeys_spec_add_ec(config->pks, argv[i]);
+ }
+ }
+ return NULL;
+}
+
+static const char *md_config_set_notify_cmd(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+ sc->mc->notify_cmd = arg;
+ (void)mconfig;
+ return NULL;
+}
+
+static const char *md_config_set_msg_cmd(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ return err;
+ }
+ sc->mc->message_cmd = arg;
+ (void)mconfig;
+ return NULL;
+}
+
+static const char *md_config_set_dns01_cmd(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+
+ if (inside_md_section(cmd)) {
+ sc->dns01_cmd = arg;
+ } else {
+ apr_table_set(sc->mc->env, MD_KEY_CMD_DNS01, arg);
+ }
+
+ (void)mconfig;
+ return NULL;
+}
+
+static const char *md_config_add_cert_file(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err, *fpath;
+
+ (void)mconfig;
+ if ((err = md_conf_check_location(cmd, MD_LOC_MD))) return err;
+ assert(sc->current);
+ fpath = ap_server_root_relative(cmd->pool, arg);
+ if (!fpath) return apr_psprintf(cmd->pool, "certificate file not found: %s", arg);
+ if (!sc->current->cert_files) {
+ sc->current->cert_files = apr_array_make(cmd->pool, 3, sizeof(char*));
+ }
+ APR_ARRAY_PUSH(sc->current->cert_files, const char*) = fpath;
+ return NULL;
+}
+
+static const char *md_config_add_key_file(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err, *fpath;
+
+ (void)mconfig;
+ if ((err = md_conf_check_location(cmd, MD_LOC_MD))) return err;
+ assert(sc->current);
+ fpath = ap_server_root_relative(cmd->pool, arg);
+ if (!fpath) return apr_psprintf(cmd->pool, "certificate key file not found: %s", arg);
+ if (!sc->current->pkey_files) {
+ sc->current->pkey_files = apr_array_make(cmd->pool, 3, sizeof(char*));
+ }
+ APR_ARRAY_PUSH(sc->current->pkey_files, const char*) = fpath;
+ return NULL;
+}
+
+static const char *md_config_set_server_status(cmd_parms *cmd, void *dc, const char *value)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ return set_on_off(&sc->mc->server_status_enabled, value, cmd->pool);
+}
+
+static const char *md_config_set_certificate_status(cmd_parms *cmd, void *dc, const char *value)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ 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 ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ 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 ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ 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 ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ 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;
+ apr_interval_time_t delay;
+
+ (void)mconfig;
+ if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) {
+ 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;
+}
+
+static const char *md_config_set_ca_certs(cmd_parms *cmd, void *dc, const char *path)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+
+ (void)dc;
+ sc->mc->ca_certs = path;
+ return NULL;
+}
+
+static const char *md_config_set_eab(cmd_parms *cmd, void *dc,
+ const char *keyid, const char *hmac)
+{
+ md_srv_conf_t *sc = md_config_get(cmd->server);
+ const char *err;
+
+ (void)dc;
+ if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+ return err;
+ }
+ if (!hmac) {
+ if (!apr_strnatcasecmp("None", keyid)) {
+ keyid = "none";
+ }
+ else {
+ /* a JSON file keeping keyid and hmac */
+ const char *fpath;
+ apr_status_t rv;
+ md_json_t *json;
+
+ /* If only dumping the config, don't verify the file */
+ if (ap_state_query(AP_SQ_RUN_MODE) == AP_SQ_RM_CONFIG_DUMP) {
+ goto leave;
+ }
+
+ fpath = ap_server_root_relative(cmd->pool, keyid);
+ if (!fpath) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name,
+ ": Invalid file path ", keyid, NULL);
+ }
+ if (!md_file_exists(fpath, cmd->pool)) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name,
+ ": file not found: ", fpath, NULL);
+ }
+
+ rv = md_json_readf(&json, cmd->pool, fpath);
+ if (APR_SUCCESS != rv) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name,
+ ": error reading JSON file ", fpath, NULL);
+ }
+ keyid = md_json_gets(json, MD_KEY_KID, NULL);
+ if (!keyid || !*keyid) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name,
+ ": JSON does not contain '", MD_KEY_KID,
+ "' element in file ", fpath, NULL);
+ }
+ hmac = md_json_gets(json, MD_KEY_HMAC, NULL);
+ if (!hmac || !*hmac) {
+ return apr_pstrcat(cmd->pool, cmd->cmd->name,
+ ": JSON does not contain '", MD_KEY_HMAC,
+ "' element in file ", fpath, NULL);
+ }
+ }
+ }
+leave:
+ sc->ca_eab_kid = keyid;
+ sc->ca_eab_hmac = hmac;
+ return NULL;
+}
+
+const command_rec md_cmds[] = {
+ AP_INIT_TAKE_ARGV("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF,
+ "URL(s) or known name(s) of CA issuing the certificates"),
+ AP_INIT_TAKE1("MDCertificateAgreement", md_config_set_agreement, NULL, RSRC_CONF,
+ "either 'accepted' or the URL of CA Terms-of-Service agreement you accept"),
+ AP_INIT_TAKE_ARGV("MDCAChallenges", md_config_set_cha_tyes, NULL, RSRC_CONF,
+ "A list of challenge types to be used."),
+ AP_INIT_TAKE1("MDCertificateProtocol", md_config_set_ca_proto, NULL, RSRC_CONF,
+ "Protocol used to obtain/renew certificates"),
+ AP_INIT_TAKE1("MDContactEmail", md_config_set_contact, NULL, RSRC_CONF,
+ "Email address used for account registration"),
+ AP_INIT_TAKE1("MDDriveMode", md_config_set_renew_mode, NULL, RSRC_CONF,
+ "deprecated, older name for MDRenewMode"),
+ AP_INIT_TAKE1("MDRenewMode", md_config_set_renew_mode, NULL, RSRC_CONF,
+ "Controls how renewal of Managed Domain certificates shall be handled."),
+ AP_INIT_TAKE_ARGV("MDomain", md_config_set_names, NULL, RSRC_CONF,
+ "A group of server names with one certificate"),
+ AP_INIT_RAW_ARGS(MD_CMD_MD_SECTION, md_config_sec_start, NULL, RSRC_CONF,
+ "Container for a managed domain with common settings and certificate."),
+ AP_INIT_RAW_ARGS(MD_CMD_MD2_SECTION, md_config_sec_start, NULL, RSRC_CONF,
+ "Short form for <MDomainSet> container."),
+ AP_INIT_TAKE_ARGV("MDMember", md_config_sec_add_members, NULL, RSRC_CONF,
+ "Define domain name(s) part of the Managed Domain. Use 'auto' or "
+ "'manual' to enable/disable auto adding names from virtual hosts."),
+ AP_INIT_TAKE_ARGV("MDMembers", md_config_sec_add_members, NULL, RSRC_CONF,
+ "Define domain name(s) part of the Managed Domain. Use 'auto' or "
+ "'manual' to enable/disable auto adding names from virtual hosts."),
+ AP_INIT_TAKE1("MDMustStaple", md_config_set_must_staple, NULL, RSRC_CONF,
+ "Enable/Disable the Must-Staple flag for new certificates."),
+ AP_INIT_TAKE12("MDPortMap", md_config_set_port_map, NULL, RSRC_CONF,
+ "Declare the mapped ports 80 and 443 on the local server. E.g. 80:8000 "
+ "to indicate that the server port 8000 is reachable as port 80 from the "
+ "internet. Use 80:- to indicate that port 80 is not reachable from "
+ "the outside."),
+ AP_INIT_TAKE_ARGV("MDPrivateKeys", md_config_set_pkeys, NULL, RSRC_CONF,
+ "set the type and parameters for private key generation"),
+ AP_INIT_TAKE1("MDHttpProxy", md_config_set_proxy, NULL, RSRC_CONF,
+ "URL of a HTTP(S) proxy to use for outgoing connections"),
+ 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)."),
+ AP_INIT_TAKE1("MDRequireHttps", md_config_set_require_https, NULL, RSRC_CONF|OR_AUTHCFG,
+ "Redirect non-secure requests to the https: equivalent."),
+ AP_INIT_RAW_ARGS("MDNotifyCmd", md_config_set_notify_cmd, NULL, RSRC_CONF,
+ "Set the command to run when signup/renew of domain is complete."),
+ AP_INIT_TAKE1("MDBaseServer", md_config_set_base_server, NULL, RSRC_CONF,
+ "Allow managing of base server outside virtual hosts."),
+ AP_INIT_RAW_ARGS("MDChallengeDns01", md_config_set_dns01_cmd, NULL, RSRC_CONF,
+ "Set the command for setup/teardown of dns-01 challenges"),
+ AP_INIT_TAKE1("MDCertificateFile", md_config_add_cert_file, NULL, RSRC_CONF,
+ "set the static certificate (chain) file to use for this domain."),
+ AP_INIT_TAKE1("MDCertificateKeyFile", md_config_add_key_file, NULL, RSRC_CONF,
+ "set the static private key file to use for this domain."),
+ AP_INIT_TAKE1("MDServerStatus", md_config_set_server_status, NULL, RSRC_CONF,
+ "On to see Managed Domains in server-status."),
+ AP_INIT_TAKE1("MDCertificateStatus", md_config_set_certificate_status, NULL, RSRC_CONF,
+ "On to see Managed Domain expose /.httpd/certificate-status."),
+ AP_INIT_TAKE1("MDWarnWindow", md_config_set_warn_window, NULL, RSRC_CONF,
+ "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("MDCACertificateFile", md_config_set_ca_certs, NULL, RSRC_CONF,
+ "Set the CA file to use for connections"),
+ AP_INIT_TAKE12("MDExternalAccountBinding", md_config_set_eab, NULL, RSRC_CONF,
+ "Set the external account binding keyid and hmac values to use at CA"),
+ AP_INIT_TAKE1("MDRetryDelay", md_config_set_min_delay, NULL, RSRC_CONF,
+ "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)
+};
+
+apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p)
+{
+ md_srv_conf_t *sc;
+ md_mod_conf_t *mc;
+
+ sc = md_config_get(s);
+ mc = sc->mc;
+
+ mc->hsts_header = NULL;
+ 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);
+ }
+#endif
+
+ return APR_SUCCESS;
+}
+
+static md_srv_conf_t *config_get_int(server_rec *s, apr_pool_t *p)
+{
+ md_srv_conf_t *sc = (md_srv_conf_t *)ap_get_module_config(s->module_config, &md_module);
+ 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);
+ }
+ return sc;
+}
+
+md_srv_conf_t *md_config_get(server_rec *s)
+{
+ return config_get_int(s, NULL);
+}
+
+md_srv_conf_t *md_config_get_unique(server_rec *s, apr_pool_t *p)
+{
+ assert(p);
+ return config_get_int(s, p);
+}
+
+md_srv_conf_t *md_config_cget(conn_rec *c)
+{
+ return md_config_get(c->base_server);
+}
+
+const char *md_config_gets(const md_srv_conf_t *sc, md_config_var_t var)
+{
+ switch (var) {
+ case MD_CONFIG_CA_CONTACT:
+ return sc->ca_contact? sc->ca_contact : defconf.ca_contact;
+ case MD_CONFIG_CA_PROTO:
+ return sc->ca_proto? sc->ca_proto : defconf.ca_proto;
+ case MD_CONFIG_BASE_DIR:
+ return sc->mc->base_dir;
+ case MD_CONFIG_PROXY:
+ return sc->mc->proxy_url;
+ case MD_CONFIG_CA_AGREEMENT:
+ return sc->ca_agreement? sc->ca_agreement : defconf.ca_agreement;
+ case MD_CONFIG_NOTIFY_CMD:
+ return sc->mc->notify_cmd;
+ default:
+ return NULL;
+ }
+}
+
+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_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(md_timeslice_t **pspan, const md_srv_conf_t *sc, md_config_var_t var)
+{
+ switch (var) {
+ case MD_CONFIG_RENEW_WINDOW:
+ *pspan = sc->renew_window? sc->renew_window : defconf.renew_window;
+ break;
+ case MD_CONFIG_WARN_WINDOW:
+ *pspan = sc->warn_window? sc->warn_window : defconf.warn_window;
+ break;
+ default:
+ break;
+ }
+}
+
+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
new file mode 100644
index 0000000..de42169
--- /dev/null
+++ b/modules/md/mod_md_config.h
@@ -0,0 +1,132 @@
+/* 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_config_h
+#define mod_md_md_config_h
+
+struct apr_hash_t;
+struct md_store_t;
+struct md_reg_t;
+struct md_ocsp_reg_t;
+struct md_pkeys_spec_t;
+
+typedef enum {
+ MD_CONFIG_CA_CONTACT,
+ MD_CONFIG_CA_PROTO,
+ MD_CONFIG_BASE_DIR,
+ MD_CONFIG_CA_AGREEMENT,
+ MD_CONFIG_DRIVE_MODE,
+ MD_CONFIG_RENEW_WINDOW,
+ MD_CONFIG_WARN_WINDOW,
+ MD_CONFIG_TRANSITIVE,
+ MD_CONFIG_PROXY,
+ MD_CONFIG_REQUIRE_HTTPS,
+ 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;
+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 */
+ 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 */
+ int can_http; /* Does someone listen to the local port 80 equivalent? */
+ int can_https; /* Does someone listen to the local port 443 equivalent? */
+ int manage_base_server; /* If base server outside vhost may be managed */
+ 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 */
+ 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 */
+ const char *message_cmd; /* message command to execute on signup/renew/warnings */
+ struct apr_table_t *env; /* environment for operation */
+ 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 */
+ 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 {
+ const char *name;
+ const server_rec *s; /* server this config belongs to */
+ md_mod_conf_t *mc; /* global config settings */
+
+ int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */
+ md_require_t require_https; /* If MDs require https: access */
+ int renew_mode; /* mode of obtaining credentials */
+ int must_staple; /* certificates should set the OCSP Must Staple extension */
+ struct md_pkeys_spec_t *pks; /* specification for private keys */
+ md_timeslice_t *renew_window; /* time before expiration that starts renewal */
+ md_timeslice_t *warn_window; /* time before expiration that warning are sent out */
+
+ struct apr_array_header_t *ca_urls; /* urls of CAs */
+ const char *ca_contact; /* contact email registered to account */
+ 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 */
+ const char *ca_eab_kid; /* != NULL, external account binding keyid */
+ const char *ca_eab_hmac; /* != NULL, external account binding hmac */
+
+ int stapling; /* OCSP stapling enabled */
+ int staple_others; /* Provide OCSP stapling for non-MD certificates */
+
+ const char *dns01_cmd; /* DNS challenge command, override global command */
+
+ md_t *current; /* md currently defined in <MDomainSet xxx> section */
+ 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);
+void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv);
+
+extern const command_rec md_cmds[];
+
+apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p);
+
+/* Get the effective md configuration for the connection */
+md_srv_conf_t *md_config_cget(conn_rec *c);
+/* Get the effective md configuration for the server */
+md_srv_conf_t *md_config_get(server_rec *s);
+/* Get the effective md configuration for the server, but make it
+ * unique to this server_rec, so that any changes only affect this server */
+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(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
new file mode 100644
index 0000000..5565f44
--- /dev/null
+++ b/modules/md/mod_md_drive.c
@@ -0,0 +1,345 @@
+/* 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_hash.h>
+#include <apr_strings.h>
+#include <apr_date.h>
+
+#include <httpd.h>
+#include <http_core.h>
+#include <http_protocol.h>
+#include <http_request.h>
+#include <http_log.h>
+
+#include "mod_watchdog.h"
+
+#include "md.h"
+#include "md_curl.h"
+#include "md_crypt.h"
+#include "md_event.h"
+#include "md_http.h"
+#include "md_json.h"
+#include "md_status.h"
+#include "md_store.h"
+#include "md_store_fs.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_util.h"
+#include "md_version.h"
+#include "md_acme.h"
+#include "md_acme_authz.h"
+
+#include "mod_md.h"
+#include "mod_md_private.h"
+#include "mod_md_config.h"
+#include "mod_md_status.h"
+#include "mod_md_drive.h"
+
+/**************************************************************************************************/
+/* watchdog based impl. */
+
+#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_renew_ctx_t {
+ apr_pool_t *p;
+ server_rec *s;
+ md_mod_conf_t *mc;
+ ap_watchdog_t *watchdog;
+
+ apr_array_header_t *jobs;
+};
+
+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 = NULL;
+ apr_status_t rv;
+
+ 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;
+
+ job->next_run = 0;
+ if (job->finished && job->notified_renewed) {
+ /* 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->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. */
+ job->fatal_error = 1;
+ 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 has a
+ * complete set of new credentials.
+ */
+ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10052)
+ "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;
+ }
+
+ /* The (possibly configured) event handler may veto renewals. This
+ * is used in cluster installtations, see #233. */
+ rv = md_event_raise("renewing", md->name, job, result, ptemp);
+ if (APR_SUCCESS != rv) {
+ ap_log_error(APLOG_MARK, APLOG_INFO, 0, dctx->s, APLOGNO(10060)
+ "%s: event-handler for 'renewing' returned %d, preventing renewal to proceed.",
+ job->mdomain, rv);
+ goto leave;
+ }
+
+ md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg));
+ md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, job->error_runs, result, ptemp);
+ md_job_end_run(job, result);
+
+ 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_renewed) {
+ md_job_save(job, result, ptemp);
+ md_job_notify(job, "renewed", result);
+ }
+ }
+ else {
+ ap_log_error( APLOG_MARK, APLOG_ERR, result->status, dctx->s, APLOGNO(10056)
+ "processing %s: %s", job->mdomain, result->detail);
+ md_job_log_append(job, "renewal-error", result->problem, result->detail);
+ md_event_holler("errored", job->mdomain, job, result, ptemp);
+ 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()));
+ }
+ }
+
+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));
+ md_job_notify(job, "expiring", result);
+ md_job_end_run(job, result);
+ }
+
+leave:
+ if (job->dirty && result) {
+ rv = md_job_save(job, result, ptemp);
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, dctx->s, "%s: saving job props", job->mdomain);
+ }
+}
+
+int md_will_renew_cert(const md_t *md)
+{
+ if (md->renew_mode == MD_RENEW_MANUAL) {
+ return 0;
+ }
+ else if (md->renew_mode == MD_RENEW_AUTO && md->cert_files && md->cert_files->nelts) {
+ return 0;
+ }
+ return 1;
+}
+
+static apr_time_t next_run_default(void)
+{
+ /* we'd like to run at least twice a day by default */
+ return apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY / 2);
+}
+
+static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp)
+{
+ md_renew_ctx_t *dctx = baton;
+ md_job_t *job;
+ apr_time_t next_run, wait_time;
+ int i;
+
+ /* 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, dctx->s, APLOGNO(10054)
+ "md watchdog start, auto drive %d mds", dctx->jobs->nelts);
+ break;
+
+ case AP_WATCHDOG_STATE_RUNNING:
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10055)
+ "md watchdog run, auto drive %d mds", dctx->jobs->nelts);
+
+ /* 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();
+ for (i = 0; i < dctx->jobs->nelts; ++i) {
+ job = APR_ARRAY_IDX(dctx->jobs, i, md_job_t *);
+
+ if (apr_time_now() >= job->next_run) {
+ process_drive_job(dctx, job, ptemp);
+ }
+
+ if (job->next_run && job->next_run < next_run) {
+ next_run = job->next_run;
+ }
+ }
+
+ wait_time = next_run - apr_time_now();
+ if (APLOGdebug(dctx->s)) {
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10107)
+ "next run in %s", md_duration_print(ptemp, wait_time));
+ }
+ wd_set_interval(dctx->watchdog, wait_time, dctx, run_watchdog);
+ break;
+
+ case AP_WATCHDOG_STATE_STOPPING:
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10058)
+ "md watchdog stopping");
+ break;
+ }
+
+ return APR_SUCCESS;
+}
+
+apr_status_t md_renew_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p)
+{
+ apr_allocator_t *allocator;
+ md_renew_ctx_t *dctx;
+ apr_pool_t *dctxp;
+ apr_status_t rv;
+ 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 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
+ * of the original data at any time. The child which hosts the watchdog thread
+ * may also die or be recycled, which causes a new watchdog thread to run
+ * in another process with the original data.
+ *
+ * Instead, we use our store to persist changes in group STAGING. This is
+ * kept writable to child processes, but the data stored there is not live.
+ * However, mod_watchdog makes sure that we only ever have a single thread in
+ * our server (on this machine) that writes there. Other processes, e.g. informing
+ * the user about progress, only read from there.
+ *
+ * All changes during driving an MD are stored as files in MG_SG_STAGING/<MD.name>.
+ * All will have "md.json" and "job.json". There may be a range of other files used
+ * by the protocol obtaining the certificate/keys.
+ *
+ *
+ */
+ 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(10061) "mod_watchdog is required");
+ return !OK;
+ }
+
+ /* 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(&dctxp, p, NULL, allocator);
+ if (rv != APR_SUCCESS) {
+ 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_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->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_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", md->name, md->state);
+
+ 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.
+ * This will reset the driving for the MD. It may run into the same
+ * error again, or in case of race/confusion/our error/CA error, it
+ * might allow the MD to succeed by a fresh start.
+ */
+ 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.", 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;
+ }
+ }
+
+ if (!dctx->jobs->nelts) {
+ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10065)
+ "no managed domain to drive, no watchdog needed.");
+ apr_pool_destroy(dctx->p);
+ return APR_SUCCESS;
+ }
+
+ 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 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 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
new file mode 100644
index 0000000..40d6d67
--- /dev/null
+++ b/modules/md/mod_md_drive.h
@@ -0,0 +1,35 @@
+/* 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_drive_h
+#define mod_md_md_drive_h
+
+struct md_mod_conf_t;
+struct md_reg_t;
+
+typedef struct md_renew_ctx_t md_renew_ctx_t;
+
+int md_will_renew_cert(const md_t *md);
+
+/**
+ * Start driving the certificate renewal for MDs marked with watched.
+ */
+apr_status_t md_renew_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p);
+
+
+
+
+#endif /* mod_md_md_drive_h */
diff --git a/modules/md/mod_md_ocsp.c b/modules/md/mod_md_ocsp.c
new file mode 100644
index 0000000..1d1e282
--- /dev/null
+++ b/modules/md/mod_md_ocsp.c
@@ -0,0 +1,272 @@
+/* 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 <http_ssl.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));
+}
+
+int md_ocsp_prime_status(server_rec *s, apr_pool_t *p,
+ const char *id, apr_size_t id_len, const char *pem)
+{
+ md_srv_conf_t *sc;
+ const md_t *md;
+ apr_array_header_t *chain;
+ apr_status_t rv = APR_ENOENT;
+
+ sc = md_config_get(s);
+ if (!staple_here(sc)) goto cleanup;
+
+ md = ((sc->assigned && sc->assigned->nelts == 1)?
+ APR_ARRAY_IDX(sc->assigned, 0, const md_t*) : NULL);
+ chain = apr_array_make(p, 5, sizeof(md_cert_t*));
+ rv = md_cert_read_chain(chain, p, pem, strlen(pem));
+ if (APR_SUCCESS != rv) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10268) "init stapling for: %s, "
+ "unable to parse PEM data", md? md->name : s->server_hostname);
+ goto cleanup;
+ }
+ else if (chain->nelts < 2) {
+ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10269) "init stapling for: %s, "
+ "need at least 2 certificates in PEM data", md? md->name : s->server_hostname);
+ rv = APR_EINVAL;
+ goto cleanup;
+ }
+
+ rv = md_ocsp_prime(sc->mc->ocsp, id, id_len,
+ APR_ARRAY_IDX(chain, 0, md_cert_t*),
+ APR_ARRAY_IDX(chain, 1, md_cert_t*), md);
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, s, "init stapling for: %s",
+ md? md->name : s->server_hostname);
+
+cleanup:
+ return (APR_SUCCESS == rv)? OK : DECLINED;
+}
+
+typedef struct {
+ unsigned char *der;
+ apr_size_t der_len;
+} ocsp_copy_ctx_t;
+
+int md_ocsp_provide_status(server_rec *s, conn_rec *c,
+ const char *id, apr_size_t id_len,
+ ap_ssl_ocsp_copy_resp *cb, void *userdata)
+{
+ 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(cb, userdata, sc->mc->ocsp, id, id_len, c->pool, md);
+ if (APR_STATUS_IS_ENOENT(rv)) goto declined;
+ return OK;
+
+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 0000000..a3f9502
--- /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
+
+
+int md_ocsp_prime_status(server_rec *s, apr_pool_t *p,
+ const char *id, apr_size_t id_len, const char *pem);
+
+int md_ocsp_provide_status(server_rec *s, conn_rec *c, const char *id, apr_size_t id_len,
+ ap_ssl_ocsp_copy_resp *cb, void *userdata);
+
+/**
+ * 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_os.c b/modules/md/mod_md_os.c
new file mode 100644
index 0000000..06a5bee
--- /dev/null
+++ b/modules/md/mod_md_os.c
@@ -0,0 +1,88 @@
+/* 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_strings.h>
+
+#include <mpm_common.h>
+#include <httpd.h>
+#include <http_log.h>
+#include <ap_mpm.h>
+
+#if APR_HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#if AP_NEED_SET_MUTEX_PERMS
+#include "unixd.h"
+#endif
+
+#include "md_util.h"
+#include "mod_md_os.h"
+
+apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool_t *p)
+{
+#if AP_NEED_SET_MUTEX_PERMS && HAVE_UNISTD_H
+ /* Since we only switch user when running as root, we only need to chown directories
+ * in that case. Otherwise, the server will ignore any "user/group" directives and
+ * child processes have the same privileges as the parent.
+ */
+ if (!geteuid()) {
+ if (-1 == chown(fname, (uid_t)uid, (gid_t)gid)) {
+ apr_status_t rv = APR_FROM_OS_ERROR(errno);
+ if (!APR_STATUS_IS_ENOENT(rv)) {
+ ap_log_perror(APLOG_MARK, APLOG_ERR, rv, p, APLOGNO(10082)
+ "Can't change owner of %s", fname);
+ }
+ return rv;
+ }
+ }
+ return APR_SUCCESS;
+#else
+ return APR_ENOTIMPL;
+#endif
+}
+
+apr_status_t md_make_worker_accessible(const char *fname, apr_pool_t *p)
+{
+#ifdef WIN32
+ return APR_ENOTIMPL;
+#else
+ return md_try_chown(fname, ap_unixd_config.user_id, -1, p);
+#endif
+}
+
+#ifdef WIN32
+
+apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s)
+{
+ return APR_ENOTIMPL;
+}
+
+#else
+
+apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s)
+{
+ apr_status_t rv;
+
+ (void)p;
+ (void)s;
+ rv = (kill(getppid(), AP_SIG_GRACEFUL) < 0)? APR_ENOTIMPL : APR_SUCCESS;
+ ap_log_error(APLOG_MARK, APLOG_TRACE1, errno, NULL, "sent signal to parent");
+ return rv;
+}
+
+#endif
+
diff --git a/modules/md/mod_md_os.h b/modules/md/mod_md_os.h
new file mode 100644
index 0000000..3085076
--- /dev/null
+++ b/modules/md/mod_md_os.h
@@ -0,0 +1,37 @@
+/* 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_os_h
+#define mod_md_md_os_h
+
+/**
+ * Try chown'ing the file/directory. Give id -1 to not change uid/gid.
+ * Will return APR_ENOTIMPL on platforms not supporting this operation.
+ */
+apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool_t *p);
+
+/**
+ * Make a file or directory read/write(/searchable) by httpd workers.
+ */
+apr_status_t md_make_worker_accessible(const char *fname, apr_pool_t *p);
+
+/**
+ * Trigger a graceful restart of the server. Depending on the architecture, may
+ * return APR_ENOTIMPL.
+ */
+apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s);
+
+#endif /* mod_md_md_os_h */
diff --git a/modules/md/mod_md_private.h b/modules/md/mod_md_private.h
new file mode 100644
index 0000000..45521ea
--- /dev/null
+++ b/modules/md/mod_md_private.h
@@ -0,0 +1,24 @@
+/* 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_private_h
+#define mod_md_md_private_h
+
+extern module AP_MODULE_DECLARE_DATA md_module;
+
+APLOG_USE_MODULE(md);
+
+#endif
diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c
new file mode 100644
index 0000000..2286051
--- /dev/null
+++ b/modules/md/mod_md_status.c
@@ -0,0 +1,987 @@
+/* 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_protocol.h>
+#include <http_request.h>
+#include <http_log.h>
+
+#include "mod_status.h"
+
+#include "md.h"
+#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"
+#include "md_store_fs.h"
+#include "md_log.h"
+#include "md_reg.h"
+#include "md_util.h"
+#include "md_version.h"
+#include "md_acme.h"
+#include "md_acme_authz.h"
+
+#include "mod_md.h"
+#include "mod_md_private.h"
+#include "mod_md_config.h"
+#include "mod_md_drive.h"
+#include "mod_md_status.h"
+
+/**************************************************************************************************/
+/* Certificate status */
+
+#define APACHE_PREFIX "/.httpd/"
+#define MD_STATUS_RESOURCE APACHE_PREFIX"certificate-status"
+#define HTML_STATUS(X) (!((X)->flags & AP_STATUS_SHORT))
+
+int md_http_cert_status(request_rec *r)
+{
+ int i;
+ md_json_t *resp, *mdj, *cj;
+ const md_srv_conf_t *sc;
+ const md_t *md;
+ md_pkey_spec_t *spec;
+ const char *keyname;
+ apr_bucket_brigade *bb;
+ apr_status_t rv;
+
+ if (!r->parsed_uri.path || strcmp(MD_STATUS_RESOURCE, r->parsed_uri.path))
+ return DECLINED;
+
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+ "requesting status for: %s", r->hostname);
+
+ /* We are looking for information about a staged certificate */
+ sc = ap_get_module_config(r->server->module_config, &md_module);
+ if (!sc || !sc->mc || !sc->mc->reg || !sc->mc->certificate_status_enabled) return DECLINED;
+ md = md_get_by_domain(sc->mc->mds, r->hostname);
+ if (!md) return DECLINED;
+
+ if (r->method_number != M_GET) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+ "md(%s): status supports only GET", md->name);
+ return HTTP_NOT_IMPLEMENTED;
+ }
+
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+ "requesting status for MD: %s", md->name);
+
+ 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;
+ }
+
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+ "status for MD: %s is %s", md->name, md_json_writep(mdj, r->pool, MD_JSON_FMT_INDENT));
+
+ resp = md_json_create(r->pool);
+
+ if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_VALID, NULL)) {
+ md_json_setj(md_json_getj(mdj, MD_KEY_CERT, MD_KEY_VALID, NULL), resp, MD_KEY_VALID, NULL);
+ }
+
+ for (i = 0; i < md_cert_count(md); ++i) {
+ spec = md_pkeys_spec_get(md->pks, i);
+ keyname = md_pkey_spec_name(spec);
+ cj = md_json_create(r->pool);
+
+ if (md_json_has_key(mdj, MD_KEY_CERT, keyname, MD_KEY_VALID, NULL)) {
+ md_json_setj(md_json_getj(mdj, MD_KEY_CERT, keyname, MD_KEY_VALID, NULL),
+ cj, MD_KEY_VALID, NULL);
+ }
+
+ if (md_json_has_key(mdj, MD_KEY_CERT, keyname, MD_KEY_SERIAL, NULL)) {
+ md_json_sets(md_json_gets(mdj, MD_KEY_CERT, keyname, MD_KEY_SERIAL, NULL),
+ cj, MD_KEY_SERIAL, NULL);
+ }
+ if (md_json_has_key(mdj, MD_KEY_CERT, keyname, MD_KEY_SHA256_FINGERPRINT, NULL)) {
+ md_json_sets(md_json_gets(mdj, MD_KEY_CERT, keyname, MD_KEY_SHA256_FINGERPRINT, NULL),
+ cj, MD_KEY_SHA256_FINGERPRINT, NULL);
+ }
+ md_json_setj(cj, resp, keyname, NULL );
+ }
+
+ if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) {
+ /* copy over the information we want to make public about this:
+ * - when not finished, add an empty object to indicate something is going on
+ * - when a certificate is staged, add the information from that */
+ cj = md_json_getj(mdj, MD_KEY_RENEWAL, MD_KEY_CERT, NULL);
+ cj = cj? cj : md_json_create(r->pool);
+ md_json_setj(cj, resp, MD_KEY_RENEWAL, MD_KEY_CERT, NULL);
+ }
+
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "md[%s]: sending status", md->name);
+ apr_table_set(r->headers_out, "Content-Type", "application/json");
+ bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+ md_json_writeb(resp, MD_JSON_FMT_INDENT, bb);
+ ap_pass_brigade(r->output_filters, bb);
+ apr_brigade_cleanup(bb);
+
+ return DONE;
+}
+
+/**************************************************************************************************/
+/* Status hook */
+
+typedef struct {
+ apr_pool_t *p;
+ const md_mod_conf_t *mc;
+ apr_bucket_brigade *bb;
+ int flags;
+ const char *prefix;
+ const char *separator;
+} status_ctx;
+
+typedef struct status_info status_info;
+
+static void add_json_val(status_ctx *ctx, md_json_t *j);
+
+typedef void add_status_fn(status_ctx *ctx, md_json_t *mdj, const status_info *info);
+
+struct status_info {
+ const char *label;
+ const char *key;
+ add_status_fn *fn;
+};
+
+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, info->key, NULL)) {
+ case MD_S_INCOMPLETE:
+ s = md_json_gets(mdj, MD_KEY_STATE_DESCR, NULL);
+ s = s? apr_psprintf(ctx->p, "incomplete: %s", s) : "incomplete";
+ break;
+ case MD_S_EXPIRED_DEPRECATED:
+ 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;
+ }
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, s);
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%s: %s\n",
+ ctx->prefix, info->label, s);
+ }
+}
+
+static void si_val_url(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ const char *url, *s;
+
+ s = url = md_json_gets(mdj, info->key, NULL);
+ if (!url) return;
+ s = md_get_ca_name_from_url(ctx->p, url);
+ if (HTML_STATUS(ctx)) {
+ 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));
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n",
+ ctx->prefix, info->label, s);
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n",
+ ctx->prefix, info->label, url);
+ }
+}
+
+static void print_date(status_ctx *ctx, apr_time_t timestamp, const char *title)
+{
+ apr_bucket_brigade *bb = ctx->bb;
+ 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(ts2)-1, "%Y-%m-%d", &texp);
+ ts[len] = '\0';
+ if (!title) {
+ apr_strftime(ts2, &len, sizeof(ts)-1, "%Y-%m-%dT%H:%M:%SZ", &texp);
+ ts2[len] = '\0';
+ title = ts2;
+ }
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_printf(bb, NULL, NULL,
+ "<span title='%s' style='white-space: nowrap;'>%s</span>",
+ ap_escape_html2(bb->p, title, 1), ts);
+ }
+ else {
+ apr_brigade_printf(bb, NULL, NULL, "%s%s: %s\n",
+ ctx->prefix, title, ts);
+ }
+ }
+}
+
+static void print_time(status_ctx *ctx, const char *label, apr_time_t t)
+{
+ apr_bucket_brigade *bb = ctx->bb;
+ 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;
+ if (HTML_STATUS(ctx)) {
+ 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(bb, NULL, NULL, "%s%s<span title='%s' "
+ "style='white-space: nowrap;'>%s</span>",
+ label, sep, ts, ts2);
+ }
+ else {
+ 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);
+ }
+ }
+ else {
+ delta = t - now;
+ apr_brigade_printf(bb, NULL, NULL, "%s%s: %" APR_TIME_T_FMT "\n",
+ ctx->prefix, label, apr_time_sec(delta));
+ }
+}
+
+static void si_val_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ const char *sfrom, *suntil, *sep, *title;
+ apr_time_t from, until;
+
+ 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 (HTML_STATUS(ctx)) {
+ if (from > apr_time_now()) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "from ");
+ print_date(ctx, 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, until, title);
+ }
+ }
+ else {
+ if (from > apr_time_now()) {
+ print_date(ctx, from,
+ apr_pstrcat(ctx->p, info->label, "From", NULL));
+ }
+ if (until) {
+ print_date(ctx, from,
+ apr_pstrcat(ctx->p, info->label, "Until", NULL));
+ }
+ }
+}
+
+static void si_add_header(status_ctx *ctx, const status_info *info)
+{
+ if (HTML_STATUS(ctx)) {
+ 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_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 val_url_print(status_ctx *ctx, const status_info *info,
+ const char*url, const char *proto, int i)
+{
+ const char *s;
+
+ if (proto && !strcmp(proto, "tailscale")) {
+ s = "tailscale";
+ }
+ else if (url) {
+ s = md_get_ca_name_from_url(ctx->p, url);
+ }
+ else {
+ return;
+ }
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s<a href='%s'>%s</a>",
+ i? " " : "",
+ ap_escape_html2(ctx->p, url, 1),
+ ap_escape_html2(ctx->p, s, 1));
+ }
+ else if (i == 0) {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n",
+ ctx->prefix, info->label, s);
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n",
+ ctx->prefix, info->label, url);
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName%d: %s\n",
+ ctx->prefix, info->label, i, s);
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL%d: %s\n",
+ ctx->prefix, info->label, i, url);
+ }
+}
+
+static void si_val_ca_urls(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ md_json_t *jcert;
+ const char *proto, *url;
+ apr_array_header_t *urls;
+ int i;
+
+ jcert = md_json_getj(mdj, info->key, NULL);
+ if (!jcert) {
+ return;
+ }
+
+ proto = md_json_gets(jcert, MD_KEY_PROTO, NULL);
+ url = md_json_gets(jcert, MD_KEY_URL, NULL);
+ if (url) {
+ /* print the effective CA url used, if set */
+ val_url_print(ctx, info, url, proto, 0);
+ }
+ else {
+ /* print the available CA urls configured */
+ urls = apr_array_make(ctx->p, 3, sizeof(const char*));
+ md_json_getsa(urls, jcert, MD_KEY_URLS, NULL);
+ for (i = 0; i < urls->nelts; ++i) {
+ url = APR_ARRAY_IDX(urls, i, const char*);
+ val_url_print(ctx, info, url, proto, i);
+ }
+ }
+}
+
+static int count_certs(void *baton, const char *key, md_json_t *json)
+{
+ int *pcount = baton;
+
+ (void)json;
+ if (strcmp(key, MD_KEY_VALID)) {
+ *pcount += 1;
+ }
+ return 1;
+}
+
+static void print_job_summary(status_ctx *ctx, md_json_t *mdj, const char *key,
+ const char *separator)
+{
+ apr_bucket_brigade *bb = ctx->bb;
+ char buffer[HUGE_STRING_LEN];
+ apr_status_t rv;
+ int finished, errors, cert_count;
+ apr_time_t t;
+ const char *s, *line;
+
+ if (!md_json_has_key(mdj, key, NULL)) {
+ return;
+ }
+
+ finished = md_json_getb(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) {
+ char *errstr = apr_strerror(rv, buffer, sizeof(buffer));
+ s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_PROBLEM, NULL);
+ if (HTML_STATUS(ctx)) {
+ line = apr_psprintf(bb->p, "%s Error[%s]: %s", line,
+ errstr, s? s : "");
+ }
+ else {
+ apr_brigade_printf(bb, NULL, NULL, "%sLastStatus: %s\n", ctx->prefix, errstr);
+ apr_brigade_printf(bb, NULL, NULL, "%sLastProblem: %s\n", ctx->prefix, s);
+ }
+ }
+
+ if (!HTML_STATUS(ctx)) {
+ apr_brigade_printf(bb, NULL, NULL, "%sFinished: %s\n", ctx->prefix,
+ finished ? "yes" : "no");
+ }
+ if (finished) {
+ cert_count = 0;
+ md_json_iterkey(count_certs, &cert_count, mdj, key, MD_KEY_CERT, NULL);
+ if (HTML_STATUS(ctx)) {
+ if (cert_count > 0) {
+ line =apr_psprintf(bb->p, "%s finished, %d new certificate%s staged.",
+ line, cert_count, cert_count > 1? "s" : "");
+ }
+ else {
+ line = apr_psprintf(bb->p, "%s finished successfully.", line);
+ }
+ }
+ else {
+ apr_brigade_printf(bb, NULL, NULL, "%sNewStaged: %d\n", ctx->prefix, cert_count);
+ }
+ }
+ else {
+ s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_DETAIL, NULL);
+ if (s) {
+ if (HTML_STATUS(ctx)) {
+ line = apr_psprintf(bb->p, "%s %s", line, s);
+ }
+ else {
+ apr_brigade_printf(bb, NULL, NULL, "%sLastDetail: %s\n", ctx->prefix, s);
+ }
+ }
+ }
+
+ errors = (int)md_json_getl(mdj, MD_KEY_ERRORS, NULL);
+ if (errors > 0) {
+ if (HTML_STATUS(ctx)) {
+ line = apr_psprintf(bb->p, "%s (%d retr%s) ", line,
+ errors, (errors > 1)? "y" : "ies");
+ }
+ else {
+ apr_brigade_printf(bb, NULL, NULL, "%sRetries: %d\n", ctx->prefix, errors);
+ }
+ }
+
+ if (HTML_STATUS(ctx)) {
+ 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(ctx,
+ HTML_STATUS(ctx) ? "\nNext run" : "NextRun",
+ t);
+ }
+ else if (line[0] != '\0') {
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(bb, NULL, NULL, "\nOngoing...");
+ }
+ else {
+ apr_brigade_printf(bb, NULL, NULL, "%s: Ongoing\n", ctx->prefix);
+ }
+ }
+}
+
+static void si_val_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ apr_time_t t;
+ const char *prefix = ctx->prefix;
+
+ (void)info;
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL);
+ }
+
+ if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) {
+ print_job_summary(ctx, 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, "Renew", t);
+ }
+ else if (t) {
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "Pending");
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s: %s", ctx->prefix, "Pending");
+ }
+ }
+ else if (MD_RENEW_MANUAL == md_json_getl(mdj, MD_KEY_RENEW_MODE, NULL)) {
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "Manual renew");
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s: %s", ctx->prefix, "Manual renew");
+ }
+ }
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = prefix;
+ }
+}
+
+static int cert_check_iter(void *baton, const char *key, md_json_t *json)
+{
+ status_ctx *ctx = baton;
+ const char *fingerprint;
+
+ fingerprint = md_json_gets(json, MD_KEY_SHA256_FINGERPRINT, NULL);
+ if (fingerprint) {
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_printf(ctx->bb, NULL, NULL,
+ "<a href=\"%s%s\">%s[%s]</a><br>",
+ ctx->mc->cert_check_url, fingerprint,
+ ctx->mc->cert_check_name, key);
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL,
+ "%sType: %s\n",
+ ctx->prefix,
+ key);
+ apr_brigade_printf(ctx->bb, NULL, NULL,
+ "%sName: %s\n",
+ ctx->prefix,
+ ctx->mc->cert_check_name);
+ apr_brigade_printf(ctx->bb, NULL, NULL,
+ "%sURL: %s%s\n",
+ ctx->prefix,
+ ctx->mc->cert_check_url, fingerprint);
+ apr_brigade_printf(ctx->bb, NULL, NULL,
+ "%sFingerprint: %s\n",
+ ctx->prefix,
+ fingerprint);
+ }
+ }
+ return 1;
+}
+
+static void si_val_remote_check(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ (void)info;
+ if (ctx->mc->cert_check_name && ctx->mc->cert_check_url) {
+ const char *prefix = ctx->prefix;
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL);
+ }
+ md_json_iterkey(cert_check_iter, ctx, mdj, MD_KEY_CERT, NULL);
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = prefix;
+ }
+ }
+}
+
+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;
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "on");
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s: on", ctx->prefix);
+ }
+}
+
+static int json_iter_val(void *data, size_t index, md_json_t *json)
+{
+ status_ctx *ctx = data;
+ const char *prefix = ctx->prefix;
+ if (HTML_STATUS(ctx)) {
+ if (index) apr_brigade_puts(ctx->bb, NULL, NULL, ctx->separator);
+ }
+ else {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL);
+ }
+ add_json_val(ctx, json);
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = prefix;
+ }
+ return 1;
+}
+
+static void add_json_val(status_ctx *ctx, md_json_t *j)
+{
+ if (!j) return;
+ if (md_json_is(MD_JSON_TYPE_ARRAY, j, NULL)) {
+ md_json_itera(json_iter_val, ctx, j, NULL);
+ return;
+ }
+ if (!HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, ctx->prefix);
+ apr_brigade_puts(ctx->bb, NULL, NULL, ": ");
+ }
+ if (md_json_is(MD_JSON_TYPE_INT, j, NULL)) {
+ md_json_writeb(j, MD_JSON_FMT_COMPACT, ctx->bb);
+ }
+ else if (md_json_is(MD_JSON_TYPE_STRING, j, NULL)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, md_json_gets(j, NULL));
+ }
+ 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");
+ }
+ if (!HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "\n");
+ }
+}
+
+static void si_val_names(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ const char *prefix = ctx->prefix;
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "<div style=\"max-width:400px;\">");
+ }
+ else {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL);
+ }
+ add_json_val(ctx, md_json_getj(mdj, info->key, NULL));
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "</div>");
+ }
+ else {
+ ctx->prefix = prefix;
+ }
+}
+
+static void add_status_cell(status_ctx *ctx, md_json_t *mdj, const status_info *info)
+{
+ if (info->fn) {
+ info->fn(ctx, mdj, info);
+ }
+ else {
+ const char *prefix = ctx->prefix;
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL);
+ }
+ add_json_val(ctx, md_json_getj(mdj, info->key, NULL));
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = prefix;
+ }
+ }
+}
+
+static const status_info status_infos[] = {
+ { "Domain", MD_KEY_NAME, NULL },
+ { "Names", MD_KEY_DOMAINS, si_val_names },
+ { "Status", MD_KEY_STATE, si_val_status },
+ { "Valid", MD_KEY_CERT, si_val_cert_valid_time },
+ { "CA", MD_KEY_CA, si_val_ca_urls },
+ { "Stapling", MD_KEY_STAPLING, si_val_stapling },
+ { "CheckAt", 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;
+ const char *prefix = ctx->prefix;
+ int i;
+
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "<tr class=\"%s\">", (index % 2)? "odd" : "even");
+ for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) {
+ apr_brigade_puts(ctx->bb, NULL, NULL, "<td>");
+ add_status_cell(ctx, mdj, &status_infos[i]);
+ apr_brigade_puts(ctx->bb, NULL, NULL, "</td>");
+ }
+ apr_brigade_puts(ctx->bb, NULL, NULL, "</tr>");
+ } else {
+ for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL);
+ add_status_cell(ctx, mdj, &status_infos[i]);
+ ctx->prefix = prefix;
+ }
+ }
+ return 1;
+}
+
+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_domains_status_hook(request_rec *r, int flags)
+{
+ const md_srv_conf_t *sc;
+ const md_mod_conf_t *mc;
+ int i;
+ status_ctx ctx;
+ 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;
+ if (!mc || !mc->server_status_enabled) return DECLINED;
+
+ ctx.p = r->pool;
+ ctx.mc = mc;
+ ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+ ctx.flags = flags;
+ ctx.prefix = "ManagedCertificates";
+ ctx.separator = " ";
+
+ mds = apr_array_copy(r->pool, mc->mds);
+ qsort(mds->elts, (size_t)mds->nelts, sizeof(md_t *), md_name_cmp);
+
+ if (!HTML_STATUS(&ctx)) {
+ int total = 0, complete = 0, renewing = 0, errored = 0, ready = 0;
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html managed domain status summary");
+ 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 managed domain status summary");
+ total = (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL);
+ complete = (int)md_json_getl(jstock, MD_KEY_COMPLETE, NULL);
+ renewing = (int)md_json_getl(jstock, MD_KEY_RENEWING, NULL);
+ errored = (int)md_json_getl(jstock, MD_KEY_ERRORED, NULL);
+ ready = (int)md_json_getl(jstock, MD_KEY_READY, NULL);
+ }
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sTotal: %d\n", ctx.prefix, total);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sOK: %d\n", ctx.prefix, complete);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sRenew: %d\n", ctx.prefix, renewing);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sErrored: %d\n", ctx.prefix, errored);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sReady: %d\n", ctx.prefix, ready);
+ }
+ if (mc->mds->nelts > 0) {
+ md_status_get_json(&jstatus, mds, mc->reg, mc->ocsp, r->pool);
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON managed domain status");
+ if (HTML_STATUS(&ctx)) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html managed domain status table");
+ apr_brigade_puts(ctx.bb, NULL, NULL,
+ "<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) {
+ si_add_header(&ctx, &status_infos[i]);
+ }
+ apr_brigade_puts(ctx.bb, NULL, NULL, "</tr>\n</thead><tbody>");
+ }
+ else {
+ ctx.prefix = "ManagedDomain";
+ }
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "iterating JSON managed domain status");
+ md_json_itera(add_md_row, &ctx, jstatus, MD_KEY_MDS, NULL);
+ if (HTML_STATUS(&ctx)) {
+ 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 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;
+ const char *prefix = ctx->prefix;
+
+ (void)info;
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL);
+ }
+ t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL);
+ print_time(ctx, "Refresh", t);
+ print_job_summary(ctx, mdj, MD_KEY_RENEWAL, ": ");
+ if (!HTML_STATUS(ctx)) {
+ ctx->prefix = prefix;
+ }
+}
+
+static const status_info ocsp_status_infos[] = {
+ { "Domain", MD_KEY_DOMAIN, NULL },
+ { "CertificateID", MD_KEY_ID, NULL },
+ { "OCSPStatus", MD_KEY_STATUS, NULL },
+ { "StaplingValid", 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;
+ const char *prefix = ctx->prefix;
+ int i;
+
+ if (HTML_STATUS(ctx)) {
+ 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>");
+ } else {
+ for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) {
+ ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL);
+ add_status_cell(ctx, mdj, &ocsp_status_infos[i]);
+ ctx->prefix = prefix;
+ }
+ }
+ 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;
+ 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;
+
+ ctx.p = r->pool;
+ ctx.mc = mc;
+ ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+ ctx.flags = flags;
+ ctx.prefix = "ManagedStaplings";
+ ctx.separator = " ";
+
+ if (!HTML_STATUS(&ctx)) {
+ int total = 0, good = 0, revoked = 0, unknown = 0;
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html ocsp stapling status summary");
+ if (md_ocsp_count(mc->ocsp) > 0) {
+ md_ocsp_get_summary(&jstock, mc->ocsp, r->pool);
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON ocsp stapling status summary");
+ total = (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL);
+ good = (int)md_json_getl(jstock, MD_KEY_GOOD, NULL);
+ revoked = (int)md_json_getl(jstock, MD_KEY_REVOKED, NULL);
+ unknown = (int)md_json_getl(jstock, MD_KEY_UNKNOWN, NULL);
+ }
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sTotal: %d\n", ctx.prefix, total);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sOK: %d\n", ctx.prefix, good);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sRenew: %d\n", ctx.prefix, revoked);
+ apr_brigade_printf(ctx.bb, NULL, NULL, "%sErrored: %d\n", ctx.prefix, unknown);
+ }
+ if (md_ocsp_count(mc->ocsp) > 0) {
+ md_ocsp_get_status_all(&jstatus, mc->ocsp, r->pool);
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON ocsp stapling status");
+ if (HTML_STATUS(&ctx)) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html ocsp stapling status table");
+ 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>");
+ }
+ else {
+ ctx.prefix = "ManagedStapling";
+ }
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "iterating JSON ocsp stapling status");
+ md_json_itera(add_ocsp_row, &ctx, jstatus, MD_KEY_OCSPS, NULL);
+ if (HTML_STATUS(&ctx)) {
+ 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 handlers */
+
+int md_status_handler(request_rec *r)
+{
+ const md_srv_conf_t *sc;
+ const md_mod_conf_t *mc;
+ apr_array_header_t *mds;
+ md_json_t *jstatus;
+ apr_bucket_brigade *bb;
+ const md_t *md;
+ const char *name;
+
+ if (strcmp(r->handler, "md-status")) {
+ return DECLINED;
+ }
+
+ sc = ap_get_module_config(r->server->module_config, &md_module);
+ if (!sc) return DECLINED;
+ mc = sc->mc;
+ if (!mc) return DECLINED;
+
+ if (r->method_number != M_GET) {
+ ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "md-status supports only GET");
+ return HTTP_NOT_IMPLEMENTED;
+ }
+
+ jstatus = NULL;
+ md = NULL;
+ if (r->path_info && r->path_info[0] == '/' && r->path_info[1] != '\0') {
+ name = strrchr(r->path_info, '/') + 1;
+ md = md_get_by_name(mc->mds, name);
+ if (!md) md = md_get_by_domain(mc->mds, name);
+ }
+
+ if (md) {
+ 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, mc->ocsp, r->pool);
+ }
+
+ if (jstatus) {
+ apr_table_set(r->headers_out, "Content-Type", "application/json");
+ bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+ md_json_writeb(jstatus, MD_JSON_FMT_INDENT, bb);
+ ap_pass_brigade(r->output_filters, bb);
+ apr_brigade_cleanup(bb);
+
+ return DONE;
+ }
+ return DECLINED;
+}
+
diff --git a/modules/md/mod_md_status.h b/modules/md/mod_md_status.h
new file mode 100644
index 0000000..f347826
--- /dev/null
+++ b/modules/md/mod_md_status.h
@@ -0,0 +1,27 @@
+/* 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_status_h
+#define mod_md_md_status_h
+
+int md_http_cert_status(request_rec *r);
+
+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);
+
+#endif /* mod_md_md_status_h */