From: Stefan Eissing Date: Fri, 4 Aug 2017 13:47:25 +0000 (+0000) Subject: first configure+compile version X-Git-Tag: 2.5.0-alpha~234^2~5 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=30763980f9b31f434d3ec82ec726e89a410d129f;p=apache first configure+compile version git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/trunk-md@1804123 13f79535-47bb-0310-9956-ffa450edef68 --- 30763980f9b31f434d3ec82ec726e89a410d129f diff --cc modules/md/Makefile.in index 0000000000,0000000000..4395bc3ac7 new file mode 100644 --- /dev/null +++ b/modules/md/Makefile.in @@@ -1,0 -1,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 --cc modules/md/config2.m4 index 0000000000,0000000000..62b809fd6e new file mode 100644 --- /dev/null +++ b/modules/md/config2.m4 @@@ -1,0 -1,0 +1,296 @@@ ++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=" 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.50]) ++ AC_TRY_COMPILE([#include ],[ ++#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 < 50 ++#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=" 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 ],[], ++ [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 ++mod_md.lo dnl ++md_config.lo dnl ++md_core.lo dnl ++md_crypt.lo dnl ++md_curl.lo dnl ++md_http.lo dnl ++md_json.lo dnl ++md_jws.lo dnl ++md_log.lo dnl ++md_os.lo dnl ++md_reg.lo dnl ++md_store.lo dnl ++md_store_fs.lo dnl ++md_util.lo dnl ++md_acme.lo dnl ++md_acme_acct.lo dnl ++md_acme_authz.lo dnl ++md_acme_drive.lo dnl ++" ++ ++ ++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 ++]) ++ ++# Ensure that other modules can pick up mod_md.h ++APR_ADDTO(INCLUDES, [-I\$(top_srcdir)/$modpath_current]) ++ ++ ++ ++dnl # end of module specific part ++APACHE_MODPATH_FINISH ++ diff --cc modules/md/md.h index 0000000000,0000000000..23b7543a78 new file mode 100644 --- /dev/null +++ b/modules/md/md.h @@@ -1,0 -1,0 +1,230 @@@ ++/* Copyright 2017 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 mod_md_md_h ++#define mod_md_md_h ++ ++#include "md_version.h" ++ ++struct apr_array_header_t; ++struct apr_hash_t; ++struct md_json_t; ++struct md_cert_t; ++struct md_pkey_t; ++struct md_store_t; ++ ++#define MD_TLSSNI01_DNS_SUFFIX ".acme.invalid" ++ ++typedef enum { ++ MD_S_UNKNOWN, /* MD has not been analysed yet */ ++ MD_S_INCOMPLETE, /* MD is missing necessary information, cannot go live */ ++ MD_S_COMPLETE, /* MD has all necessary information, can go live */ ++ MD_S_EXPIRED, /* MD is complete, but credentials have expired */ ++ MD_S_ERROR, /* MD data is flawed, unable to be processed as is */ ++} md_state_t; ++ ++typedef enum { ++ MD_SV_TEXT, ++ MD_SV_JSON, ++ MD_SV_CERT, ++ MD_SV_PKEY, ++ MD_SV_CHAIN, ++} md_store_vtype_t; ++ ++typedef enum { ++ MD_SG_NONE, ++ MD_SG_ACCOUNTS, ++ MD_SG_CHALLENGES, ++ MD_SG_DOMAINS, ++ MD_SG_STAGING, ++ MD_SG_ARCHIVE, ++ MD_SG_TMP, ++ MD_SG_COUNT, ++} md_store_group_t; ++ ++typedef enum { ++ MD_DRIVE_DEFAULT = -1, /* default value */ ++ MD_DRIVE_MANUAL, /* manually triggered transmission of credentials */ ++ MD_DRIVE_AUTO, /* automatic process performed by httpd */ ++ MD_DRIVE_ALWAYS, /* always driven by httpd, even if not used in any vhost */ ++} md_drive_mode_t; ++ ++typedef struct md_t md_t; ++struct md_t { ++ const char *name; /* unique name of this MD */ ++ md_state_t state; /* state of this MD */ ++ apr_time_t expires; /* When the credentials for this domain expire. 0 if unknown */ ++ apr_interval_time_t renew_window;/* time before expiration that starts renewal */ ++ ++ struct apr_array_header_t *domains; /* all DNS names this MD includes */ ++ md_drive_mode_t drive_mode; /* mode of obtaining credentials */ ++ int must_staple; /* certificates should set the OCSP Must Staple extension */ ++ ++ const char *ca_url; /* url of CA certificate service */ ++ const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ ++ 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 *contacts; /* list of contact uris, e.g. mailto:xxx */ ++ ++ const char *cert_url; /* url where cert has been created, remember during drive */ ++ ++ const char *defn_name; /* config file this MD was defined */ ++ unsigned defn_line_number; /* line number of definition */ ++}; ++ ++#define MD_KEY_ACCOUNT "account" ++#define MD_KEY_AGREEMENT "agreement" ++#define MD_KEY_CA "ca" ++#define MD_KEY_CA_URL "ca-url" ++#define MD_KEY_CERT "cert" ++#define MD_KEY_CHALLENGES "challenges" ++#define MD_KEY_CONTACT "contact" ++#define MD_KEY_CONTACTS "contacts" ++#define MD_KEY_CSR "csr" ++#define MD_KEY_DISABLED "disabled" ++#define MD_KEY_DIR "dir" ++#define MD_KEY_DOMAIN "domain" ++#define MD_KEY_DOMAINS "domains" ++#define MD_KEY_DRIVE_MODE "drive-mode" ++#define MD_KEY_EXPIRES "expires" ++#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_KEYAUTHZ "keyAuthorization" ++#define MD_KEY_LOCATION "location" ++#define MD_KEY_NAME "name" ++#define MD_KEY_PROTO "proto" ++#define MD_KEY_REGISTRATION "registration" ++#define MD_KEY_RENEW_WINDOW "renew-window" ++#define MD_KEY_RESOURCE "resource" ++#define MD_KEY_STATE "state" ++#define MD_KEY_STATUS "status" ++#define MD_KEY_TOKEN "token" ++#define MD_KEY_TYPE "type" ++#define MD_KEY_URL "url" ++#define MD_KEY_URI "uri" ++#define MD_KEY_VALUE "value" ++#define MD_KEY_VERSION "version" ++ ++#define MD_FN_MD "md.json" ++#define MD_FN_PKEY "pkey.pem" ++#define MD_FN_CERT "cert.pem" ++#define MD_FN_CHAIN "chain.pem" ++#define MD_FN_HTTPD_JSON "httpd.json" ++ ++/* 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))) ++ ++#define MD_SECS_PER_HOUR (60*60) ++#define MD_SECS_PER_DAY (24*MD_SECS_PER_HOUR) ++ ++/** ++ * Determine if the Managed Domain contains a specific domain name. ++ */ ++int md_contains(const md_t *md, const char *domain); ++ ++/** ++ * 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); ++ ++/** ++ * 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); ++ ++/** ++ * Find the managed domain in the list that has the most overlaps in domains to the ++ * given md. ++ */ ++md_t *md_find_closest_match(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. ++ */ ++const char *md_create(md_t **pmd, 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); ++ ++/**************************************************************************************************/ ++/* domain credentials */ ++ ++typedef struct md_creds_t md_creds_t; ++struct md_creds_t { ++ struct md_cert_t *cert; ++ struct md_pkey_t *pkey; ++ struct apr_array_header_t *chain; /* list of md_cert* */ ++ int expired; ++}; ++ ++#endif /* mod_md_md_h */ diff --cc modules/md/md_acme.c index 0000000000,0000000000..6eedb55d2a new file mode 100644 --- /dev/null +++ b/modules/md/md_acme.c @@@ -1,0 -1,0 +1,506 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++#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_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; ++ apr_status_t rv; ++}; ++ ++static acme_problem_status_t Problems[] = { ++ { "acme:error:badCSR", APR_EINVAL }, ++ { "acme:error:badNonce", APR_EAGAIN }, ++ { "acme:error:badSignatureAlgorithm", APR_EINVAL }, ++ { "acme:error:invalidContact", APR_BADARG }, ++ { "acme:error:unsupportedContact", APR_EGENERAL }, ++ { "acme:error:malformed", APR_EINVAL }, ++ { "acme:error:rateLimited", APR_BADARG }, ++ { "acme:error:rejectedIdentifier", APR_BADARG }, ++ { "acme:error:serverInternal", APR_EGENERAL }, ++ { "acme:error:unauthorized", APR_EACCES }, ++ { "acme:error:unsupportedIdentifier", APR_BADARG }, ++ { "acme:error:userActionRequired", APR_EAGAIN }, ++ { "acme:error:badRevocationReason", APR_EINVAL }, ++ { "acme:error:caa", APR_EGENERAL }, ++ { "acme:error:dns", APR_EGENERAL }, ++ { "acme:error:connection", APR_EGENERAL }, ++ { "acme:error:tls", APR_EGENERAL }, ++ { "acme:error:incorrectResponse", APR_EGENERAL }, ++}; ++ ++static apr_status_t problem_status_get(const char *type) { ++ int 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; ++} ++ ++apr_status_t md_acme_init(apr_pool_t *p, const char *base) ++{ ++ base_product = base; ++ return md_crypt_init(p); ++} ++ ++apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url) ++{ ++ 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 (Something, like certbot)", ++ base_product, MOD_MD_VERSION); ++ acme->pkey_bits = 4096; ++ acme->max_retries = 3; ++ ++ 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: ", url); ++ return APR_EINVAL; ++ } ++ ++ len = strlen(uri_parsed.hostname); ++ acme->sname = (len <= 16)? uri_parsed.hostname : apr_pstrdup(p, uri_parsed.hostname + len - 16); ++ ++ *pacme = (APR_SUCCESS == rv)? acme : NULL; ++ return rv; ++} ++ ++apr_status_t md_acme_setup(md_acme_t *acme) ++{ ++ apr_status_t rv; ++ md_json_t *json; ++ ++ assert(acme->url); ++ if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p, ++ acme->user_agent))) { ++ return rv; ++ } ++ md_http_set_response_limit(acme->http, 1024*1024); ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url); ++ ++ rv = md_acme_get_json(&json, acme, acme->url, acme->p); ++ if (APR_SUCCESS == rv) { ++ acme->new_authz = md_json_gets(json, "new-authz", NULL); ++ acme->new_cert = md_json_gets(json, "new-cert", NULL); ++ acme->new_reg = md_json_gets(json, "new-reg", NULL); ++ acme->revoke_cert = md_json_gets(json, "revoke-cert", NULL); ++ if (acme->new_authz && acme->new_cert && acme->new_reg && acme->revoke_cert) { ++ return APR_SUCCESS; ++ } ++ rv = APR_EINVAL; ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* 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) ++{ ++ if (res->headers) { ++ const char *nonce = apr_table_get(res->headers, "Replay-Nonce"); ++ if (nonce) { ++ md_acme_t *acme = res->req->baton; ++ acme->nonce = apr_pstrdup(acme->p, nonce); ++ } ++ } ++ return res->rv; ++} ++ ++static apr_status_t md_acme_new_nonce(md_acme_t *acme) ++{ ++ apr_status_t rv; ++ long id; ++ ++ rv = md_http_HEAD(acme->http, acme->new_reg, NULL, http_update_nonce, acme, &id); ++ md_http_await(acme->http, id); ++ return rv; ++} ++ ++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; ++ } ++ ++ 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_hdrs = apr_table_make(pool, 5); ++ if (!req->prot_hdrs) { ++ apr_pool_destroy(pool); ++ return NULL; ++ } ++ req->max_retries = acme->max_retries; ++ ++ return req; ++} ++ ++apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *jpayload) ++{ ++ const char *payload; ++ size_t payload_len; ++ ++ if (!req->acme->acct) { ++ return APR_EINVAL; ++ } ++ ++ payload = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); ++ if (!payload) { ++ return APR_EINVAL; ++ } ++ ++ payload_len = strlen(payload); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, ++ "acct payload(len=%d): %s", payload_len, payload); ++ return md_jws_sign(&req->req_json, req->p, payload, payload_len, ++ req->prot_hdrs, req->acme->acct_key, NULL); ++} ++ ++ ++static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res) ++{ ++ const char *ctype; ++ md_json_t *problem; ++ ++ ctype = apr_table_get(req->resp_hdrs, "content-type"); ++ if (ctype && !strcmp(ctype, "application/problem+json")) { ++ /* RFC 7807 */ ++ md_json_read_http(&problem, req->p, res); ++ if (problem) { ++ const char *ptype, *pdetail; ++ ++ req->resp_json = problem; ++ ptype = md_json_gets(problem, "type", NULL); ++ pdetail = md_json_gets(problem, "detail", NULL); ++ req->rv = problem_status_get(ptype); ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, req->rv, req->p, ++ "acme problem %s: %s", ptype, pdetail); ++ return req->rv; ++ } ++ } ++ ++ if (APR_SUCCESS == res->rv) { ++ switch (res->status) { ++ case 400: ++ return APR_EINVAL; ++ case 403: ++ return APR_EACCES; ++ case 404: ++ return APR_ENOENT; ++ default: ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p, ++ "acme problem unknonw: http status %d", res->status); ++ return APR_EGENERAL; ++ } ++ } ++ return res->rv; ++} ++ ++/**************************************************************************************************/ ++/* ACME requests with nonce handling */ ++ ++static apr_status_t md_acme_req_done(md_acme_req_t *req) ++{ ++ apr_status_t rv = req->rv; ++ if (req->p) { ++ apr_pool_destroy(req->p); ++ } ++ return rv; ++} ++ ++static apr_status_t on_response(const md_http_response_t *res) ++{ ++ md_acme_req_t *req = res->req->baton; ++ apr_status_t rv = res->rv; ++ ++ if (APR_SUCCESS != rv) { ++ goto out; ++ } ++ ++ 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 : ""); ++ } ++ 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_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, ++ "response: %d, content-type=%s", res->status, ++ apr_table_get(res->headers, "Content-Type")); ++ } ++ } ++ else if (APR_EAGAIN == (rv = inspect_problem(req, res))) { ++ /* leave req alive */ ++ return rv; ++ } ++ ++out: ++ md_acme_req_done(req); ++ return rv; ++} ++ ++static apr_status_t md_acme_req_send(md_acme_req_t *req) ++{ ++ apr_status_t rv; ++ md_acme_t *acme = req->acme; ++ const char *body = NULL; ++ ++ assert(acme->url); ++ ++ if (strcmp("GET", req->method) && strcmp("HEAD", req->method)) { ++ if (!acme->new_authz) { ++ if (APR_SUCCESS != (rv = md_acme_setup(acme))) { ++ return rv; ++ } ++ } ++ if (!acme->nonce) { ++ if (APR_SUCCESS != (rv = md_acme_new_nonce(acme))) { ++ return rv; ++ } ++ } ++ ++ apr_table_set(req->prot_hdrs, "nonce", acme->nonce); ++ acme->nonce = NULL; ++ } ++ ++ rv = req->on_init? req->on_init(req, req->baton) : APR_SUCCESS; ++ ++ if ((rv == APR_SUCCESS) && req->req_json) { ++ body = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT); ++ if (!body) { ++ rv = APR_EINVAL; ++ } ++ } ++ ++ if (rv == APR_SUCCESS) { ++ long id = 0; ++ ++ if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p, ++ "req: POST %s, body:\n%s", req->url, body); ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, ++ "req: POST %s", req->url); ++ } ++ if (!strcmp("GET", req->method)) { ++ rv = md_http_GET(req->acme->http, req->url, NULL, on_response, req, &id); ++ } ++ else if (!strcmp("POST", req->method)) { ++ rv = md_http_POSTd(req->acme->http, req->url, NULL, "application/json", ++ body, body? strlen(body) : 0, on_response, req, &id); ++ } ++ else if (!strcmp("HEAD", req->method)) { ++ rv = md_http_HEAD(req->acme->http, req->url, NULL, on_response, req, &id); ++ } ++ 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"); ++ md_http_await(acme->http, id); ++ ++ if (APR_EAGAIN == rv && req->max_retries > 0) { ++ --req->max_retries; ++ return md_acme_req_send(req); ++ } ++ req = NULL; ++ } ++ ++ if (req) { ++ md_acme_req_done(req); ++ } ++ 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, ++ 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->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, ++ 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->baton = baton; ++ ++ return md_acme_req_send(req); ++} ++ ++/**************************************************************************************************/ ++/* 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; ++ ++ 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, &ctx); ++ *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; ++ return rv; ++} ++ diff --cc modules/md/md_acme.h index 0000000000,0000000000..a5a57aab88 new file mode 100644 --- /dev/null +++ b/modules/md/md_acme.h @@@ -1,0 -1,0 +1,260 @@@ ++/* Copyright 2017 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 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_proto_t; ++struct md_store_t; ++ ++#define MD_PROTO_ACME "ACME" ++ ++#define MD_AUTHZ_CHA_HTTP_01 "http-01" ++#define MD_AUTHZ_CHA_SNI_01 "tls-sni-01" ++ ++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; ++ ++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; ++ struct md_acme_acct_t *acct; ++ struct md_pkey_t *acct_key; ++ ++ const char *new_authz; ++ const char *new_cert; ++ const char *new_reg; ++ const char *revoke_cert; ++ ++ struct md_http_t *http; ++ ++ const char *nonce; ++ int max_retries; ++ unsigned int pkey_bits; ++}; ++ ++/** ++ * Global init, call once at start up. ++ */ ++apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version); ++ ++/** ++ * Create a new ACME server instance. If path is not NULL, will use that directory ++ * for persisting information. Will load any inforation 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 ++ */ ++apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url); ++ ++/** ++ * 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); ++ ++/**************************************************************************************************/ ++/* account handling */ ++ ++#define MD_ACME_ACCT_STAGED "staged" ++ ++apr_status_t md_acme_acct_load(struct md_acme_acct_t **pacct, struct md_pkey_t **ppkey, ++ struct md_store_t *store, md_store_group_t group, ++ const char *name, apr_pool_t *p); ++ ++/** ++ * Specify the account to use by name in local store. On success, the account ++ * the "current" one used by the acme instance. ++ */ ++apr_status_t md_acme_use_acct(md_acme_t *acme, struct md_store_t *store, ++ apr_pool_t *p, const char *acct_id); ++ ++apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, ++ md_t *md, apr_pool_t *p); ++ ++/** ++ * 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_get_acct(md_acme_t *acme, 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 ++ * accounces 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. ++ */ ++apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, const char *agreement); ++ ++/** ++ * 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(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); ++ ++/** ++ * Create a new account at the ACME server. The ++ * new account is the one used by the acme instance afterwards, on success. ++ */ ++apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, ++ const char *agreement); ++ ++apr_status_t md_acme_acct_save(struct md_store_t *store, apr_pool_t *p, md_acme_t *acme, ++ struct md_acme_acct_t *acct, struct md_pkey_t *acct_key); ++ ++apr_status_t md_acme_save(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); ++ ++apr_status_t md_acme_acct_save_staged(md_acme_t *acme, struct md_store_t *store, ++ md_t *md, apr_pool_t *p); ++ ++/** ++ * Delete the current account at the ACME server and remove it from store. ++ */ ++apr_status_t md_acme_delete_acct(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); ++ ++/** ++ * Delete the account from the local store without contacting the ACME server. ++ */ ++apr_status_t md_acme_unstore_acct(struct md_store_t *store, apr_pool_t *p, const char *acct_id); ++ ++/**************************************************************************************************/ ++/* request handling */ ++ ++/** ++ * Request callback on a successfull 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); ++ ++/** ++ * A request against an ACME server ++ */ ++typedef struct md_acme_req_t md_acme_req_t; ++ ++/** ++ * 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 successfull 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); ++ ++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 */ ++ apr_table_t *prot_hdrs; /* JWS headers needing protection (nonce) */ ++ 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 recevied */ ++ ++ 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 */ ++ int max_retries; /* how often this might be retried */ ++ void *baton; /* userdata for callbacks */ ++}; ++ ++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, ++ 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, ++ void *baton); ++ ++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, ++ 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); ++ ++#endif /* md_acme_h */ diff --cc modules/md/md_acme_acct.c index 0000000000,0000000000..71246c3f86 new file mode 100644 --- /dev/null +++ b/modules/md/md_acme_acct.c @@@ -1,0 -1,0 +1,652 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "md.h" ++#include "md_crypt.h" ++#include "md_json.h" ++#include "md_jws.h" ++#include "md_log.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, const char *id, apr_array_header_t *contacts) ++{ ++ md_acme_acct_t *acct; ++ ++ acct = apr_pcalloc(p, sizeof(*acct)); ++ ++ acct->id = id? apr_pstrdup(p, id) : NULL; ++ 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 void md_acme_acct_free(md_acme_acct_t *acct) ++{ ++} ++ ++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_json_t *acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) ++{ ++ md_json_t *jacct; ++ ++ assert(acct); ++ jacct = md_json_create(p); ++ md_json_sets(acct->id, jacct, MD_KEY_ID, NULL); ++ md_json_setb(acct->disabled, jacct, MD_KEY_DISABLED, NULL); ++ md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); ++ md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); ++ md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); ++ if (acct->agreement) { ++ md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); ++ } ++ ++ return jacct; ++} ++ ++static apr_status_t 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; ++ int disabled; ++ const char *ca_url, *url, *id; ++ apr_array_header_t *contacts; ++ ++ id = md_json_gets(json, MD_KEY_ID, NULL); ++ disabled = md_json_getb(json, MD_KEY_DISABLED, NULL); ++ 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", id); ++ goto out; ++ } ++ ++ 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: %s", id); ++ goto out; ++ } ++ ++ contacts = apr_array_make(p, 5, sizeof(const char *)); ++ md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL); ++ rv = acct_make(&acct, p, ca_url, id, contacts); ++ if (APR_SUCCESS == rv) { ++ acct->disabled = disabled; ++ acct->url = url; ++ acct->agreement = md_json_gets(json, "terms-of-service", NULL); ++ } ++ ++out: ++ *pacct = (APR_SUCCESS == rv)? acct : NULL; ++ return rv; ++} ++ ++apr_status_t md_acme_acct_save_staged(md_acme_t *acme, md_store_t *store, md_t *md, apr_pool_t *p) ++{ ++ md_acme_acct_t *acct = acme->acct; ++ md_json_t *jacct; ++ apr_status_t rv; ++ ++ jacct = acct_to_json(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_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, ++ md_acme_acct_t *acct, md_pkey_t *acct_key) ++{ ++ md_json_t *jacct; ++ apr_status_t rv; ++ int i; ++ const char *id; ++ ++ jacct = acct_to_json(acct, p); ++ id = acct->id; ++ ++ 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); ++ md_json_sets(id, jacct, MD_KEY_ID, NULL); ++ rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1); ++ } ++ ++ } ++ if (APR_SUCCESS == rv) { ++ acct->id = 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_save(md_acme_t *acme, md_store_t *store, apr_pool_t *p) ++{ ++ return md_acme_acct_save(store, p, acme, acme->acct, acme->acct_key); ++} ++ ++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 = 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, 0, p, "loading key: %s", name); ++ goto out; ++ } ++ } ++out: ++ if (APR_SUCCESS != rv) { ++ *pacct = NULL; ++ *ppkey = NULL; ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* Lookup */ ++ ++typedef struct { ++ apr_pool_t *p; ++ md_acme_t *acme; ++ 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_json_t *json = value; ++ int disabled; ++ const char *ca_url, *id; ++ ++ id = md_json_gets(json, MD_KEY_ID, NULL); ++ disabled = md_json_getb(json, MD_KEY_DISABLED, NULL); ++ ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL); ++ ++ if (!disabled && ca_url && !strcmp(ctx->acme->url, ca_url)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, ++ "found account %s for %s: %s, disabled=%d, ca-url=%s", ++ name, ctx->acme->url, id, disabled, ca_url); ++ ctx->id = id; ++ return 0; ++ } ++ return 1; ++} ++ ++static apr_status_t acct_find(md_acme_acct_t **pacct, md_pkey_t **ppkey, ++ md_store_t *store, md_acme_t *acme, apr_pool_t *p) ++{ ++ apr_status_t rv; ++ find_ctx ctx; ++ ++ ctx.p = p; ++ ctx.acme = acme; ++ ctx.id = NULL; ++ ++ rv = md_store_iter(find_acct, &ctx, store, p, MD_SG_ACCOUNTS, mk_acct_pattern(p, acme), ++ MD_FN_ACCOUNT, MD_SV_JSON); ++ if (ctx.id) { ++ rv = md_acme_acct_load(pacct, ppkey, store, MD_SG_ACCOUNTS, ctx.id, p); ++ } ++ else { ++ *pacct = NULL; ++ rv = APR_ENOENT; ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "acct_find %s", (*pacct)? (*pacct)->id : "NULL"); ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* Register a new account */ ++ ++typedef struct { ++ md_acme_t *acme; ++ apr_pool_t *p; ++} acct_ctx_t; ++ ++static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton) ++{ ++ acct_ctx_t *ctx = baton; ++ md_json_t *jpayload; ++ ++ jpayload = md_json_create(req->p); ++ md_json_sets("new-reg", jpayload, MD_KEY_RESOURCE, NULL); ++ md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL); ++ if (ctx->acme->acct->agreement) { ++ md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL); ++ } ++ ++ return md_acme_req_body_init(req, jpayload); ++} ++ ++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 (!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); ++ } ++ if (!acct->tos_required) { ++ acct->tos_required = md_link_find_relation(hdrs, ctx->p, "terms-of-service"); ++ if (acct->tos_required) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, ++ "server requires agreement to <%s>", acct->tos_required); ++ } ++ } ++ ++ apr_array_clear(acct->contacts); ++ md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL); ++ 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; ++} ++ ++static apr_status_t acct_register(md_acme_t *acme, apr_pool_t *p, ++ apr_array_header_t *contacts, const char *agreement) ++{ ++ apr_status_t rv; ++ md_pkey_t *pkey; ++ const char *err = NULL, *uri; ++ int i; ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account"); ++ ++ if (agreement) { ++ if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, agreement, &err))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, ++ "invalid agreement uri (%s): %s", err, agreement); ++ goto out; ++ } ++ } ++ for (i = 0; i < contacts->nelts; ++i) { ++ uri = APR_ARRAY_IDX(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 (APR_SUCCESS == (rv = md_pkey_gen_rsa(&pkey, acme->p, acme->pkey_bits)) ++ && APR_SUCCESS == (rv = acct_make(&acme->acct, p, acme->url, NULL, contacts))) { ++ acct_ctx_t ctx; ++ ++ acme->acct_key = pkey; ++ if (agreement) { ++ acme->acct->agreement = agreement; ++ } ++ ++ ctx.acme = acme; ++ ctx.p = p; ++ rv = md_acme_POST(acme, acme->new_reg, on_init_acct_new, acct_upd, 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) { ++ md_acme_acct_free(acme->acct); ++ acme->acct = NULL; ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* acct validation */ ++ ++static apr_status_t on_init_acct_valid(md_acme_req_t *req, void *baton) ++{ ++ md_json_t *jpayload; ++ ++ jpayload = md_json_create(req->p); ++ md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); ++ ++ return md_acme_req_body_init(req, jpayload); ++} ++ ++static apr_status_t acct_valid(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, ++ md_json_t *body, void *baton) ++{ ++ md_acme_acct_t *acct = acme->acct; ++ apr_status_t rv = APR_SUCCESS; ++ const char *body_str; ++ const char *tos_required; ++ ++ apr_array_clear(acct->contacts); ++ md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL); ++ acct->registration = md_json_clone(acme->p, body); ++ ++ body_str = md_json_writep(body, acme->p, MD_JSON_FMT_INDENT); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "validate acct %s: %s", ++ acct->url, body_str ? body_str : ""); ++ ++ acct->agreement = md_json_gets(acct->registration, MD_KEY_AGREEMENT, NULL); ++ tos_required = md_link_find_relation(hdrs, acme->p, "terms-of-service"); ++ ++ if (tos_required) { ++ if (!acct->agreement || strcmp(tos_required, acct->agreement)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, ++ "needs to agree to terms-of-service '%s', " ++ "has already agreed to '%s'", ++ tos_required, acct->agreement); ++ } ++ acct->tos_required = tos_required; ++ } ++ ++ return rv; ++} ++ ++static apr_status_t md_acme_validate_acct(md_acme_t *acme) ++{ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct validation"); ++ if (!acme->acct) { ++ return APR_EINVAL; ++ } ++ return md_acme_POST(acme, acme->acct->url, on_init_acct_valid, acct_valid, NULL, NULL); ++} ++ ++/**************************************************************************************************/ ++/* account setup */ ++ ++static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p) ++{ ++ apr_status_t rv; ++ ++ if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) { ++ if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv)) { ++ if (!acme->acct->disabled) { ++ acme->acct->disabled = 1; ++ if (store) { ++ md_acme_save(acme, store, p); ++ } ++ } ++ acme->acct = NULL; ++ acme->acct_key = NULL; ++ rv = APR_ENOENT; ++ } ++ } ++ return rv; ++} ++ ++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 (acct->ca_url && !strcmp(acct->ca_url, acme->url)) { ++ acme->acct = acct; ++ acme->acct_key = pkey; ++ rv = acct_validate(acme, store, p); ++ } ++ else { ++ /* account is from a nother server or, more likely, from another ++ * protocol endpoint on the same server */ ++ rv = APR_ENOENT; ++ } ++ } ++ return rv; ++} ++ ++apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, ++ 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 = acct; ++ acme->acct_key = pkey; ++ rv = acct_validate(acme, NULL, p); ++ } ++ return rv; ++} ++ ++const char *md_acme_get_acct(md_acme_t *acme, apr_pool_t *p) ++{ ++ return acme->acct? acme->acct->id : NULL; ++} ++ ++const char *md_acme_get_agreement(md_acme_t *acme) ++{ ++ return acme->acct? acme->acct->agreement : NULL; ++} ++ ++apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) ++{ ++ md_acme_acct_t *acct; ++ md_pkey_t *pkey; ++ apr_status_t rv; ++ ++ while (APR_SUCCESS == acct_find(&acct, &pkey, store, acme, acme->p)) { ++ acme->acct = acct; ++ acme->acct_key = pkey; ++ rv = acct_validate(acme, store, p); ++ ++ if (APR_SUCCESS == rv) { ++ return rv; ++ } ++ else { ++ acme->acct = NULL; ++ acme->acct_key = NULL; ++ if (!APR_STATUS_IS_ENOENT(rv)) { ++ /* encountered error with server */ ++ return rv; ++ } ++ } ++ } ++ return APR_ENOENT; ++} ++ ++apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, ++ const char *agreement) ++{ ++ return acct_register(acme, p, contacts, agreement); ++} ++ ++/**************************************************************************************************/ ++/* Delete the account */ ++ ++apr_status_t md_acme_unstore_acct(md_store_t *store, apr_pool_t *p, const char *acct_id) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ ++ rv = md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCOUNT, p, 1); ++ if (APR_SUCCESS == rv) { ++ md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1); ++ } ++ return rv; ++} ++ ++static apr_status_t on_init_acct_del(md_acme_req_t *req, void *baton) ++{ ++ md_json_t *jpayload; ++ ++ jpayload = md_json_create(req->p); ++ md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); ++ md_json_setb(1, jpayload, "delete", NULL); ++ ++ return md_acme_req_body_init(req, jpayload); ++} ++ ++static apr_status_t acct_del(md_acme_t *acme, apr_pool_t *p, ++ const apr_table_t *hdrs, md_json_t *body, void *baton) ++{ ++ md_store_t *store = baton; ++ apr_status_t rv = APR_SUCCESS; ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "deleted account %s", acme->acct->url); ++ if (store) { ++ rv = md_acme_unstore_acct(store, p, acme->acct->id); ++ acme->acct = NULL; ++ acme->acct_key = NULL; ++ } ++ return rv; ++} ++ ++apr_status_t md_acme_delete_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) ++{ ++ md_acme_acct_t *acct = acme->acct; ++ ++ 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); ++ return md_acme_POST(acme, acct->url, on_init_acct_del, acct_del, NULL, store); ++} ++ ++/**************************************************************************************************/ ++/* 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); ++ md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); ++ md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, 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; ++ ctx.acme = acme; ++ ctx.p = p; ++ return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, &ctx); ++} ++ ++static int agreement_required(md_acme_acct_t *acct) ++{ ++ return (!acct->agreement ++ || (acct->tos_required && strcmp(acct->tos_required, acct->agreement))); ++} ++ ++apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, const char *agreement) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ ++ /* Check if (correct) Terms-of-Service for account were accepted */ ++ if (agreement_required(acme->acct)) { ++ const char *tos = acme->acct->tos_required; ++ if (!tos) { ++ if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, ++ "validate for account %", acme->acct->id); ++ return rv; ++ } ++ tos = acme->acct->tos_required; ++ if (!tos) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "unknown terms-of-service " ++ "required after validation of account %", acme->acct->id); ++ return APR_EGENERAL; ++ } ++ } ++ ++ if (acme->acct->agreement && !strcmp(tos, acme->acct->agreement)) { ++ rv = md_acme_agree(acme, p, tos); ++ } ++ else if (agreement && !strcmp(tos, agreement)) { ++ rv = md_acme_agree(acme, p, tos); ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, acme->p, ++ "need to accept terms-of-service <%s> for account %s", ++ tos, acme->acct->id); ++ rv = APR_EACCES; ++ } ++ } ++ return rv; ++} diff --cc modules/md/md_acme_acct.h index 0000000000,0000000000..5abd9b5031 new file mode 100644 --- /dev/null +++ b/modules/md/md_acme_acct.h @@@ -1,0 -1,0 +1,44 @@@ ++/* Copyright 2017 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 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; ++ ++ ++/** ++ * An ACME account at an ACME server. ++ */ ++typedef struct md_acme_acct_t md_acme_acct_t; ++ ++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 */ ++ 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 */ ++ ++ struct md_json_t *registration; /* data from server registration */ ++ int disabled; ++}; ++ ++#define MD_FN_ACCOUNT "account.json" ++#define MD_FN_ACCT_KEY "account.pem" ++ ++#endif /* md_acme_acct_h */ diff --cc modules/md/md_acme_authz.c index 0000000000,0000000000..83c6ca9609 new file mode 100644 --- /dev/null +++ b/modules/md/md_acme_authz.c @@@ -1,0 -1,0 +1,682 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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_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; ++} ++ ++md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p, md_acme_t *acme) ++{ ++ md_acme_authz_set_t *authz_set; ++ ++ authz_set = apr_pcalloc(p, sizeof(*authz_set)); ++ authz_set->authzs = apr_array_make(p, 5, sizeof(md_acme_authz_t *)); ++ ++ return authz_set; ++} ++ ++md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain) ++{ ++ md_acme_authz_t *authz; ++ int i; ++ ++ assert(domain); ++ for (i = 0; i < set->authzs->nelts; ++i) { ++ authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *); ++ if (!apr_strnatcasecmp(domain, authz->domain)) { ++ return authz; ++ } ++ } ++ return NULL; ++} ++ ++apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz) ++{ ++ md_acme_authz_t *existing; ++ ++ assert(authz->domain); ++ if (NULL != (existing = md_acme_authz_set_get(set, authz->domain))) { ++ return APR_EINVAL; ++ } ++ APR_ARRAY_PUSH(set->authzs, md_acme_authz_t*) = authz; ++ return APR_SUCCESS; ++} ++ ++apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain) ++{ ++ md_acme_authz_t *authz; ++ int i; ++ ++ assert(domain); ++ for (i = 0; i < set->authzs->nelts; ++i) { ++ authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *); ++ if (!apr_strnatcasecmp(domain, authz->domain)) { ++ int n = i +1; ++ if (n < set->authzs->nelts) { ++ void **elems = (void **)set->authzs->elts; ++ memmove(elems + i, elems + n, set->authzs->nelts - n); ++ } ++ --set->authzs->nelts; ++ return APR_SUCCESS; ++ } ++ } ++ return APR_ENOENT; ++} ++ ++/**************************************************************************************************/ ++/* 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; ++} ++ ++static apr_status_t on_init_authz(md_acme_req_t *req, void *baton) ++{ ++ authz_req_ctx *ctx = baton; ++ md_json_t *jpayload; ++ ++ jpayload = md_json_create(req->p); ++ md_json_sets("new-authz", jpayload, MD_KEY_RESOURCE, NULL); ++ md_json_sets("dns", jpayload, MD_KEY_IDENTIFIER, MD_KEY_TYPE, NULL); ++ md_json_sets(ctx->domain, jpayload, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); ++ ++ return md_acme_req_body_init(req, jpayload); ++} ++ ++static apr_status_t authz_created(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, ++ md_json_t *body, void *baton) ++{ ++ authz_req_ctx *ctx = baton; ++ const char *location = apr_table_get(hdrs, "location"); ++ apr_status_t rv = APR_SUCCESS; ++ ++ if (location) { ++ ctx->authz = md_acme_authz_create(ctx->p); ++ ctx->authz->domain = apr_pstrdup(ctx->p, ctx->domain); ++ ctx->authz->location = apr_pstrdup(ctx->p, location); ++ ctx->authz->resource = md_json_clone(ctx->p, body); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ctx->p, "authz_new at %s", location); ++ } ++ else { ++ rv = APR_EINVAL; ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ctx->p, "new authz, no location header"); ++ } ++ return rv; ++} ++ ++apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, md_acme_t *acme, ++ md_store_t *store, const char *domain, apr_pool_t *p) ++{ ++ apr_status_t rv; ++ authz_req_ctx ctx; ++ ++ authz_req_ctx_init(&ctx, acme, domain, NULL, p); ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "create new authz"); ++ rv = md_acme_POST(acme, acme->new_authz, on_init_authz, authz_created, NULL, &ctx); ++ ++ *pauthz = (APR_SUCCESS == rv)? ctx.authz : NULL; ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* Update an exiosting authorization */ ++ ++apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, ++ md_store_t *store, apr_pool_t *p) ++{ ++ md_json_t *json; ++ const char *s; ++ apr_status_t rv; ++ ++ assert(acme); ++ assert(acme->http); ++ assert(authz); ++ assert(authz->location); ++ ++ if (APR_SUCCESS != (rv = md_acme_get_json(&json, acme, authz->location, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "update authz for %s at %s", ++ authz->domain, authz->location); ++ return rv; ++ } ++ ++ authz->resource = json; ++ s = md_json_gets(json, "identifier", "type", NULL); ++ if (!s || strcmp(s, "dns")) return APR_EINVAL; ++ s = md_json_gets(json, "identifier", "value", NULL); ++ if (!s || strcmp(s, authz->domain)) return APR_EINVAL; ++ ++ authz->state = MD_ACME_AUTHZ_S_UNKNOWN; ++ s = md_json_gets(json, "status", NULL); ++ if (s && !strcmp(s, "pending")) { ++ authz->state = MD_ACME_AUTHZ_S_PENDING; ++ } ++ else if (s && !strcmp(s, "valid")) { ++ authz->state = MD_ACME_AUTHZ_S_VALID; ++ } ++ else if (s && !strcmp(s, "invalid")) { ++ authz->state = MD_ACME_AUTHZ_S_INVALID; ++ } ++ else if (s) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "unknown authz state '%s' " ++ "for %s in %s", s, authz->domain, authz->location); ++ return APR_EINVAL; ++ } ++ 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); ++ 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) ++{ ++ authz_req_ctx *ctx = baton; ++ md_json_t *jpayload; ++ ++ jpayload = md_json_create(req->p); ++ md_json_sets("challenge", jpayload, MD_KEY_RESOURCE, NULL); ++ md_json_sets(ctx->challenge->key_authz, jpayload, MD_KEY_KEYAUTHZ, NULL); ++ ++ 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; ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "updated authz %s", ctx->authz->location); ++ 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; ++ ++ 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, apr_pool_t *p) ++{ ++ const char *data; ++ apr_status_t rv; ++ int notify_server; ++ ++ if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_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)) { ++ rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01, ++ MD_SV_TEXT, (void*)cha->key_authz, 0); ++ authz->dir = authz->domain; ++ notify_server = 1; ++ } ++ ++ if (APR_SUCCESS == rv && notify_server) { ++ authz_req_ctx ctx; ++ ++ /* 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, &ctx); ++ } ++out: ++ return rv; ++} ++ ++static apr_status_t setup_cha_dns(const char **pdns, md_acme_authz_cha_t *cha, apr_pool_t *p) ++{ ++ const char *dhex; ++ char *dns; ++ apr_size_t dhex_len; ++ apr_status_t rv; ++ ++ rv = md_crypt_sha256_digest_hex(&dhex, p, cha->key_authz, strlen(cha->key_authz)); ++ if (APR_SUCCESS == rv) { ++ dhex = md_util_str_tolower((char*)dhex); ++ dhex_len = strlen(dhex); ++ assert(dhex_len > 32); ++ dns = apr_pcalloc(p, dhex_len + 1 + sizeof(MD_TLSSNI01_DNS_SUFFIX)); ++ strncpy(dns, dhex, 32); ++ dns[32] = '.'; ++ strncpy(dns+33, dhex+32, dhex_len-32); ++ memcpy(dns+(dhex_len+1), MD_TLSSNI01_DNS_SUFFIX, sizeof(MD_TLSSNI01_DNS_SUFFIX)); ++ } ++ *pdns = (APR_SUCCESS == rv)? dns : NULL; ++ return rv; ++} ++ ++static apr_status_t cha_tls_sni_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, ++ md_acme_t *acme, md_store_t *store, apr_pool_t *p) ++{ ++ md_cert_t *cha_cert; ++ md_pkey_t *cha_key; ++ const char *cha_dns; ++ apr_status_t rv; ++ int notify_server; ++ ++ if ( APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server)) ++ || APR_SUCCESS != (rv = setup_cha_dns(&cha_dns, cha, p))) { ++ goto out; ++ } ++ ++ rv = md_store_load(store, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT, ++ MD_SV_CERT, (void**)&cha_cert, p); ++ if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, cha_dns)) ++ || APR_STATUS_IS_ENOENT(rv)) { ++ ++ if (APR_SUCCESS != (rv = md_pkey_gen_rsa(&cha_key, p, acme->pkey_bits))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-sni-01 challgenge key", ++ authz->domain); ++ goto out; ++ } ++ ++ /* setup a certificate containing the challenge dns */ ++ rv = md_cert_self_sign(&cha_cert, authz->domain, cha_dns, cha_key, ++ apr_time_from_sec(7 * MD_SECS_PER_DAY), p); ++ ++ if (APR_SUCCESS != rv) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: setup self signed cert for %s", ++ authz->domain, cha_dns); ++ goto out; ++ } ++ ++ rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_PKEY, ++ MD_SV_PKEY, (void*)cha_key, 0); ++ if (APR_SUCCESS == rv) { ++ rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT, ++ MD_SV_CERT, (void*)cha_cert, 0); ++ } ++ authz->dir = cha_dns; ++ notify_server = 1; ++ } ++ ++ if (APR_SUCCESS == rv && notify_server) { ++ authz_req_ctx ctx; ++ ++ /* 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, &ctx); ++ } ++out: ++ return rv; ++} ++ ++typedef apr_status_t cha_starter(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, ++ md_acme_t *acme, md_store_t *store, apr_pool_t *p); ++ ++typedef struct { ++ const char *name; ++ cha_starter *start; ++} cha_type; ++ ++static const cha_type CHA_TYPES[] = { ++ { MD_AUTHZ_TYPE_HTTP01, cha_http_01_setup }, ++ { MD_AUTHZ_TYPE_TLSSNI01, cha_tls_sni_01_setup }, ++}; ++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 = md_json_gets(json, MD_KEY_TYPE, NULL); ++ if (ctype) { ++ 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, apr_pool_t *p) ++{ ++ apr_status_t rv; ++ int i; ++ cha_find_ctx fctx; ++ ++ assert(acme); ++ assert(authz); ++ assert(authz->resource); ++ ++ fctx.p = p; ++ fctx.accepted = NULL; ++ ++ /* Look in the order challenge types are defined */ ++ for (i = 0; i < challenges->nelts && !fctx.accepted; ++i) { ++ fctx.type = APR_ARRAY_IDX(challenges, i, const char *); ++ md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); ++ } ++ ++ if (!fctx.accepted) { ++ 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_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, ++ "%s: the server offers no ACME challenge that is configured " ++ "for this MD. The server offered '%s' and available for this " ++ "MD are: '%s' (via %s).", ++ authz->domain, ++ apr_array_pstrcat(p, fctx.offered, ' '), ++ apr_array_pstrcat(p, challenges, ' '), ++ authz->location); ++ return rv; ++ } ++ ++ for (i = 0; i < CHA_TYPES_LEN; ++i) { ++ if (!apr_strnatcasecmp(CHA_TYPES[i].name, fctx.accepted->type)) { ++ return CHA_TYPES[i].start(fctx.accepted, authz, acme, store, p); ++ } ++ } ++ ++ rv = APR_ENOTIMPL; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, ++ "%s: no implementation found for challenge '%s'", ++ authz->domain, fctx.accepted->type); ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* Delete an existing authz resource */ ++ ++typedef struct { ++ apr_pool_t *p; ++ md_acme_authz_t *authz; ++} del_ctx; ++ ++static apr_status_t on_init_authz_del(md_acme_req_t *req, void *baton) ++{ ++ md_json_t *jpayload; ++ ++ jpayload = md_json_create(req->p); ++ md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL); ++ ++ return md_acme_req_body_init(req, jpayload); ++} ++ ++static apr_status_t authz_del(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, ++ md_json_t *body, void *baton) ++{ ++ authz_req_ctx *ctx = baton; ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "deleted authz %s", ctx->authz->location); ++ acme->acct = NULL; ++ return APR_SUCCESS; ++} ++ ++apr_status_t md_acme_authz_del(md_acme_authz_t *authz, md_acme_t *acme, ++ md_store_t *store, apr_pool_t *p) ++{ ++ authz_req_ctx ctx; ++ ++ ctx.p = p; ++ ctx.authz = authz; ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "delete authz for %s from %s", ++ authz->domain, authz->location); ++ return md_acme_POST(acme, authz->location, on_init_authz_del, authz_del, NULL, &ctx); ++} ++ ++/**************************************************************************************************/ ++/* authz conversion */ ++ ++md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p) ++{ ++ md_json_t *json = md_json_create(p); ++ if (json) { ++ md_json_sets(a->domain, json, MD_KEY_DOMAIN, NULL); ++ md_json_sets(a->location, json, MD_KEY_LOCATION, NULL); ++ md_json_sets(a->dir, json, MD_KEY_DIR, NULL); ++ md_json_setl(a->state, json, MD_KEY_STATE, NULL); ++ return json; ++ } ++ return NULL; ++} ++ ++md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p) ++{ ++ md_acme_authz_t *authz = md_acme_authz_create(p); ++ if (authz) { ++ authz->domain = md_json_dups(p, json, MD_KEY_DOMAIN, NULL); ++ authz->location = md_json_dups(p, json, MD_KEY_LOCATION, NULL); ++ authz->dir = md_json_dups(p, json, MD_KEY_DIR, NULL); ++ authz->state = (int)md_json_getl(json, MD_KEY_STATE, NULL); ++ return authz; ++ } ++ return NULL; ++} ++ ++/**************************************************************************************************/ ++/* authz_set conversion */ ++ ++#define MD_KEY_ACCOUNT "account" ++#define MD_KEY_AUTHZS "authorizations" ++ ++static apr_status_t authz_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) ++{ ++ return md_json_setj(md_acme_authz_to_json(value, p), json, NULL); ++} ++ ++static apr_status_t authz_from_json(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) ++{ ++ *pvalue = md_acme_authz_from_json(json, p); ++ return (*pvalue)? APR_SUCCESS : APR_EINVAL; ++} ++ ++md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p) ++{ ++ md_json_t *json = md_json_create(p); ++ if (json) { ++ md_json_seta(set->authzs, authz_to_json, NULL, json, MD_KEY_AUTHZS, NULL); ++ return json; ++ } ++ return NULL; ++} ++ ++md_acme_authz_set_t *md_acme_authz_set_from_json(md_json_t *json, apr_pool_t *p) ++{ ++ md_acme_authz_set_t *set = md_acme_authz_set_create(p, NULL); ++ if (set) { ++ md_json_geta(set->authzs, authz_from_json, NULL, json, MD_KEY_AUTHZS, NULL); ++ return set; ++ } ++ return NULL; ++} ++ ++/**************************************************************************************************/ ++/* persistence */ ++ ++apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, ++ const char *md_name, md_acme_authz_set_t **pauthz_set, ++ apr_pool_t *p) ++{ ++ apr_status_t rv; ++ md_json_t *json; ++ md_acme_authz_set_t *authz_set; ++ ++ rv = md_store_load_json(store, group, md_name, MD_FN_AUTHZ, &json, p); ++ if (APR_SUCCESS == rv) { ++ authz_set = md_acme_authz_set_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_authz_set_t *set; ++ const char *md_name; ++ int create; ++ ++ group = va_arg(ap, int); ++ md_name = va_arg(ap, const char *); ++ set = va_arg(ap, md_acme_authz_set_t *); ++ create = va_arg(ap, int); ++ ++ json = md_acme_authz_set_to_json(set, ptemp); ++ assert(json); ++ return md_store_save_json(store, ptemp, group, md_name, MD_FN_AUTHZ, json, create); ++} ++ ++apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p, ++ md_store_group_t group, const char *md_name, ++ md_acme_authz_set_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_authz_set_t *authz_set; ++ const md_acme_authz_t *authz; ++ md_store_group_t group; ++ const char *md_name; ++ int i; ++ ++ group = va_arg(ap, int); ++ md_name = va_arg(ap, const char *); ++ ++ if (APR_SUCCESS == md_acme_authz_set_load(store, group, md_name, &authz_set, p)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz_set loaded for %s", md_name); ++ for (i = 0; i < authz_set->authzs->nelts; ++i) { ++ authz = APR_ARRAY_IDX(authz_set->authzs, i, const md_acme_authz_t*); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz check %s", authz->domain); ++ if (authz->dir) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz purge %s", authz->dir); ++ md_store_purge(store, p, MD_SG_CHALLENGES, authz->dir); ++ } ++ } ++ } ++ return md_store_remove(store, group, md_name, MD_FN_AUTHZ, ptemp, 1); ++} ++ ++apr_status_t md_acme_authz_set_purge(md_store_t *store, md_store_group_t group, ++ apr_pool_t *p, const char *md_name) ++{ ++ return md_util_pool_vdo(p_purge, store, p, group, md_name, NULL); ++} ++ diff --cc modules/md/md_acme_authz.h index 0000000000,0000000000..0996deb56b new file mode 100644 --- /dev/null +++ b/modules/md/md_acme_authz.h @@@ -1,0 -1,0 +1,102 @@@ ++/* Copyright 2017 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 mod_md_md_acme_authz_h ++#define mod_md_md_acme_authz_h ++ ++struct apr_array_header_t; ++struct md_acme_t; ++struct md_acme_acct_t; ++struct md_json_t; ++struct md_store_t; ++ ++typedef struct md_acme_challenge_t md_acme_challenge_t; ++ ++/**************************************************************************************************/ ++/* authorization request for a specific domain name */ ++ ++#define MD_AUTHZ_TYPE_HTTP01 "http-01" ++#define MD_AUTHZ_TYPE_TLSSNI01 "tls-sni-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 *location; ++ const char *dir; ++ md_acme_authz_state_t state; ++ apr_time_t expires; ++ struct md_json_t *resource; ++}; ++ ++#define MD_FN_HTTP01 "acme-http-01.txt" ++#define MD_FN_TLSSNI01_CERT "acme-tls-sni-01.cert.pem" ++#define MD_FN_TLSSNI01_PKEY "acme-tls-sni-01.key.pem" ++#define MD_FN_AUTHZ "authz.json" ++ ++ ++md_acme_authz_t *md_acme_authz_create(apr_pool_t *p); ++ ++struct md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p); ++md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p); ++ ++/* authz interaction with ACME server */ ++apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, struct md_acme_t *acme, ++ struct md_store_t *store, const char *domain, apr_pool_t *p); ++ ++apr_status_t md_acme_authz_update(md_acme_authz_t *authz, struct md_acme_t *acme, ++ struct md_store_t *store, 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, apr_pool_t *p); ++apr_status_t md_acme_authz_del(md_acme_authz_t *authz, struct md_acme_t *acme, ++ struct md_store_t *store, apr_pool_t *p); ++ ++/**************************************************************************************************/ ++/* set of authz data for a managed domain */ ++ ++typedef struct md_acme_authz_set_t md_acme_authz_set_t; ++ ++struct md_acme_authz_set_t { ++ struct apr_array_header_t *authzs; ++}; ++ ++md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p, struct md_acme_t *acme); ++md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain); ++apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz); ++apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain); ++ ++struct md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p); ++md_acme_authz_set_t *md_acme_authz_set_from_json(struct md_json_t *json, apr_pool_t *p); ++ ++apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, ++ const char *md_name, md_acme_authz_set_t **pauthz_set, ++ apr_pool_t *p); ++apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p, ++ md_store_group_t group, const char *md_name, ++ md_acme_authz_set_t *authz_set, int create); ++ ++apr_status_t md_acme_authz_set_purge(struct md_store_t *store, md_store_group_t group, ++ apr_pool_t *p, const char *md_name); ++ ++#endif /* md_acme_authz_h */ diff --cc modules/md/md_acme_drive.c index 0000000000,0000000000..f72e6bd730 new file mode 100644 --- /dev/null +++ b/modules/md/md_acme_drive.c @@@ -1,0 -1,0 +1,926 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++#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_reg.h" ++#include "md_store.h" ++#include "md_util.h" ++ ++#include "md_acme.h" ++#include "md_acme_acct.h" ++#include "md_acme_authz.h" ++ ++typedef struct { ++ md_proto_driver_t *driver; ++ ++ const char *phase; ++ int complete; ++ ++ md_pkey_t *pkey; ++ md_cert_t *cert; ++ apr_array_header_t *chain; ++ ++ md_acme_t *acme; ++ md_t *md; ++ const md_creds_t *ncreds; ++ ++ apr_array_header_t *ca_challenges; ++ md_acme_authz_set_t *authz_set; ++ apr_interval_time_t authz_monitor_timeout; ++ ++ const char *csr_der_64; ++ apr_interval_time_t cert_poll_timeout; ++ ++ const char *chain_url; ++ ++} md_acme_driver_t; ++ ++/**************************************************************************************************/ ++/* account setup */ ++ ++static apr_status_t ad_set_acct(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ md_t *md = ad->md; ++ apr_status_t rv = APR_SUCCESS; ++ int update = 0, acct_installed = 0; ++ ++ ad->phase = "setup acme"; ++ if (!ad->acme ++ && APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, md->ca_url))) { ++ goto out; ++ } ++ ++ ad->phase = "choose account"; ++ /* Do we have a staged (modified) account? */ ++ if (APR_SUCCESS == (rv = md_acme_use_acct_staged(ad->acme, d->store, md, d->p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-using staged account"); ++ md->ca_account = MD_ACME_ACCT_STAGED; ++ acct_installed = 1; ++ } ++ else if (APR_STATUS_IS_ENOENT(rv)) { ++ rv = APR_SUCCESS; ++ } ++ ++ /* Get an account for the ACME server for this MD */ ++ if (md->ca_account && !acct_installed) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-use account '%s'", md->ca_account); ++ rv = md_acme_use_acct(ad->acme, d->store, d->p, md->ca_account); ++ 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 = 1; ++ rv = APR_SUCCESS; ++ } ++ } ++ ++ if (APR_SUCCESS == rv && !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 == md_acme_find_acct(ad->acme, d->store, d->p)) { ++ md->ca_account = md_acme_get_acct(ad->acme, d->p); ++ update = 1; ++ } ++ } ++ ++ if (APR_SUCCESS == rv && !md->ca_account) { ++ /* 2.2 No local account exists, create a new one */ ++ 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)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, ++ "no contact information for md %s", md->name); ++ rv = APR_EINVAL; ++ goto out; ++ } ++ ++ if (APR_SUCCESS == (rv = md_acme_create_acct(ad->acme, d->p, ++ md->contacts, md->ca_agreement)) ++ && APR_SUCCESS == (rv = md_acme_acct_save_staged(ad->acme, d->store, md, d->p))) { ++ md->ca_account = MD_ACME_ACCT_STAGED; ++ update = 1; ++ } ++ } ++ ++out: ++ if (APR_SUCCESS == rv) { ++ const char *agreement = md_acme_get_agreement(ad->acme); ++ /* Persist the account chosen at the md so we use the same on future runs */ ++ if (agreement && (!md->ca_agreement || strcmp(agreement, md->ca_agreement))) { ++ md->ca_agreement = agreement; ++ update = 1; ++ } ++ if (update) { ++ rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); ++ } ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* authz/challenge setup */ ++ ++/** ++ * Pre-Req: we have an account for the ACME server that has accepted the current license agreement ++ * For each domain in MD: ++ * - check if there already is a valid AUTHZ resource ++ * - if ot, create an AUTHZ resource with challenge data ++ */ ++static apr_status_t ad_setup_authz(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv; ++ md_t *md = ad->md; ++ md_acme_authz_t *authz; ++ int i, changed; ++ ++ assert(ad->md); ++ assert(ad->acme); ++ ++ ad->phase = "check authz"; ++ ++ /* 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 ++ */ ++ rv = md_acme_authz_set_load(d->store, MD_SG_STAGING, md->name, &ad->authz_set, d->p); ++ if (!ad->authz_set || APR_STATUS_IS_ENOENT(rv)) { ++ ad->authz_set = md_acme_authz_set_create(d->p, ad->acme); ++ rv = APR_SUCCESS; ++ } ++ else if (APR_SUCCESS != rv) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: loading authz data", md->name); ++ md_acme_authz_set_purge(d->store, MD_SG_STAGING, d->p, md->name); ++ return APR_EAGAIN; ++ } ++ ++ /* Remove anything we no longer need */ ++ for (i = 0; i < ad->authz_set->authzs->nelts; ++i) { ++ authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); ++ if (!md_contains(md, authz->domain)) { ++ md_acme_authz_set_remove(ad->authz_set, authz->domain); ++ changed = 1; ++ } ++ } ++ ++ /* Add anything we do not already have */ ++ for (i = 0; i < md->domains->nelts && APR_SUCCESS == rv; ++i) { ++ const char *domain = APR_ARRAY_IDX(md->domains, i, const char *); ++ changed = 0; ++ authz = md_acme_authz_set_get(ad->authz_set, domain); ++ if (authz) { ++ /* check valid */ ++ rv = md_acme_authz_update(authz, ad->acme, d->store, d->p); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: updated authz for %s", ++ md->name, domain); ++ if (APR_SUCCESS != rv) { ++ md_acme_authz_set_remove(ad->authz_set, domain); ++ authz = NULL; ++ changed = 1; ++ } ++ } ++ if (!authz) { ++ /* create new one */ ++ rv = md_acme_authz_register(&authz, ad->acme, d->store, domain, d->p); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: created authz for %s", ++ md->name, domain); ++ if (APR_SUCCESS == rv) { ++ rv = md_acme_authz_set_add(ad->authz_set, authz); ++ changed = 1; ++ } ++ } ++ } ++ ++ /* Save any changes */ ++ if (APR_SUCCESS == rv && changed) { ++ rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, md->name, ad->authz_set, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", md->name); ++ } ++ ++ return rv; ++} ++ ++/** ++ * Pre-Req: all domains have a AUTHZ resources at the ACME server ++ * For each domain in MD: ++ * - if AUTHZ resource is 'valid' -> continue ++ * - if AUTHZ resource is 'pending': ++ * - find preferred challenge choice ++ * - calculate challenge data for httpd to find ++ * - POST challenge start to ACME server ++ * For each domain in MD where AUTHZ is 'pending', until overall timeout: ++ * - wait a certain time, check status again ++ * If not all AUTHZ are valid, fail ++ */ ++static apr_status_t ad_start_challenges(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv = APR_SUCCESS; ++ md_acme_authz_t *authz; ++ int i, changed = 0; ++ ++ assert(ad->md); ++ assert(ad->acme); ++ assert(ad->authz_set); ++ ++ ad->phase = "start challenges"; ++ ++ for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) { ++ authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s", ++ ad->md->name, authz->domain); ++ if (APR_SUCCESS != (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: check authz for %s", ++ ad->md->name, authz->domain); ++ break; ++ } ++ ++ switch (authz->state) { ++ case MD_ACME_AUTHZ_S_VALID: ++ break; ++ case MD_ACME_AUTHZ_S_PENDING: ++ ++ rv = md_acme_authz_respond(authz, ad->acme, d->store, ad->ca_challenges, d->p); ++ changed = 1; ++ break; ++ default: ++ rv = APR_EINVAL; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, ++ "%s: unexpected AUTHZ state %d at %s", ++ authz->domain, authz->state, authz->location); ++ break; ++ } ++ } ++ ++ if (APR_SUCCESS == rv && changed) { ++ rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->authz_set, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", ad->md->name); ++ } ++ return rv; ++} ++ ++static apr_status_t check_challenges(void *baton, int attemmpt) ++{ ++ md_proto_driver_t *d = baton; ++ md_acme_driver_t *ad = d->baton; ++ md_acme_authz_t *authz; ++ apr_status_t rv = APR_SUCCESS; ++ int i; ++ ++ for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) { ++ authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s", ++ ad->md->name, authz->domain); ++ if (APR_SUCCESS == (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) { ++ switch (authz->state) { ++ case MD_ACME_AUTHZ_S_VALID: ++ break; ++ case MD_ACME_AUTHZ_S_PENDING: ++ rv = APR_EAGAIN; ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, ++ "%s: status pending at %s", authz->domain, authz->location); ++ break; ++ default: ++ rv = APR_EINVAL; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, ++ "%s: unexpected AUTHZ state %d at %s", ++ authz->domain, authz->state, authz->location); ++ break; ++ } ++ } ++ } ++ return rv; ++} ++ ++static apr_status_t ad_monitor_challenges(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv; ++ ++ assert(ad->md); ++ assert(ad->acme); ++ assert(ad->authz_set); ++ ++ ad->phase = "monitor challenges"; ++ rv = md_util_try(check_challenges, d, 0, ad->authz_monitor_timeout, 0, 0, 1); ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, d->p, ++ "%s: checked all domain authorizations", ad->md->name); ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* poll cert */ ++ ++ ++static apr_status_t read_http_cert(md_cert_t **pcert, apr_pool_t *p, ++ const md_http_response_t *res) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ ++ if (APR_SUCCESS != (rv = md_cert_read_http(pcert, 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); ++ } ++ return rv; ++} ++ ++static apr_status_t on_got_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; ++ ++ ++ if (APR_SUCCESS == (rv = read_http_cert(&ad->cert, d->p, res))) { ++ rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CERT, ++ MD_SV_CERT, ad->cert, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved"); ++ } ++ 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; ++ ++ return md_acme_GET(ad->acme, ad->md->cert_url, NULL, NULL, on_got_cert, d); ++} ++ ++static apr_status_t ad_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->md->cert_url); ++ ++ ad->phase = "poll 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_INFO, 0, d->p, "poll for cert at %s", ad->md->cert_url); ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* cert setup */ ++ ++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("new-cert", jpayload, MD_KEY_RESOURCE, NULL); ++ 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; ++ apr_status_t rv = APR_SUCCESS; ++ ++ ad->md->cert_url = apr_table_get(res->headers, "location"); ++ if (!ad->md->cert_url) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, ++ "cert created without giving its location header"); ++ return APR_EINVAL; ++ } ++ if (APR_SUCCESS != (rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, ++ "%s: saving cert url %s", ad->md->name, ad->md->cert_url); ++ return rv; ++ } ++ ++ /* Check if it already was sent with this response */ ++ if (APR_SUCCESS == (rv = md_cert_read_http(&ad->cert, d->p, res))) { ++ rv = md_cert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->cert, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved"); ++ } ++ else if (APR_STATUS_IS_ENOENT(rv)) { ++ rv = APR_SUCCESS; ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, ++ "cert not in response, need to poll %s", ad->md->cert_url); ++ } ++ ++ return rv; ++} ++ ++/** ++ * Pre-Req: all domains have been validated by the ACME server, e.g. all have AUTHZ ++ * resources that have status 'valid' ++ * - 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 ++ */ ++static apr_status_t ad_setup_certificate(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ md_pkey_t *pkey; ++ apr_status_t rv; ++ ++ ad->phase = "setup cert pkey"; ++ ++ rv = md_pkey_load(d->store, MD_SG_STAGING, ad->md->name, &pkey, d->p); ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ if (APR_SUCCESS == (rv = md_pkey_gen_rsa(&pkey, d->p, ad->acme->pkey_bits))) { ++ rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, ad->md->name, pkey, 1); ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: generate pkey", ad->md->name); ++ } ++ ++ if (APR_SUCCESS == rv) { ++ ad->phase = "setup csr"; ++ rv = md_cert_req_create(&ad->csr_der_64, ad->md, pkey, d->p); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create CSR", ad->md->name); ++ } ++ ++ if (APR_SUCCESS == rv) { ++ ad->phase = "submit csr"; ++ rv = md_acme_POST(ad->acme, ad->acme->new_cert, on_init_csr_req, NULL, csr_req, d); ++ } ++ ++ if (APR_SUCCESS == rv) { ++ if (!ad->cert) { ++ rv = ad_cert_poll(d, 0); ++ } ++ } ++ 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; ++ md_cert_t *cert; ++ const char *ct; ++ ++ ct = apr_table_get(res->headers, "Content-Type"); ++ if (ct && !strcmp("application/x-pkcs7-mime", ct)) { ++ /* root cert most likely, end it here */ ++ return APR_SUCCESS; ++ } ++ ++ if (APR_SUCCESS == (rv = read_http_cert(&cert, d->p, res))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain cert parsed"); ++ APR_ARRAY_PUSH(ad->chain, md_cert_t *) = cert; ++ } ++ 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; ++ md_cert_t *cert; ++ const char *url, *last_url = NULL; ++ apr_status_t rv = APR_SUCCESS; ++ ++ while (APR_SUCCESS == rv && ad->chain->nelts < 10) { ++ int nelts = ad->chain->nelts; ++ if (ad->chain && nelts > 0) { ++ cert = APR_ARRAY_IDX(ad->chain, nelts - 1, md_cert_t *); ++ } ++ else { ++ cert = ad->cert; ++ } ++ ++ if (APR_SUCCESS == (rv = md_cert_get_issuers_uri(&url, cert, d->p)) ++ && (!last_url || strcmp(last_url, url))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "next issuer is %s", url); ++#if MD_EXPERIMENTAL ++ if (!strncmp("http://127.0.0.1:", url, sizeof("http://127.0.0.1:")-1)) { ++ /* test boulder instance always reports issuer cert on localhost, but we ++ * may use a different address to reach the boulder server */ ++ apr_uri_t curi, ca; ++ ++ if (APR_SUCCESS == apr_uri_parse(d->p, url, &curi) ++ && APR_SUCCESS == apr_uri_parse(d->p, ad->acme->url, &ca)) { ++ url = apr_psprintf(d->p, "%s://%s:%s%s", ++ ca.scheme, ca.hostname, ca.port_str, curi.path); ++ } ++ } ++#endif ++ rv = md_acme_GET(ad->acme, url, NULL, NULL, on_add_chain, d); ++ ++ if (APR_SUCCESS == rv && nelts == ad->chain->nelts) { ++ break; ++ } ++ } ++ else if (APR_STATUS_IS_ENOENT(rv) || !url || !strlen(url)) { ++ rv = APR_SUCCESS; ++ break; ++ } ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, ++ "got chain with %d certs", ad->chain->nelts); ++ return rv; ++} ++ ++static apr_status_t ad_chain_install(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv; ++ ++ ad->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *)); ++ if (APR_SUCCESS == (rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0))) { ++ rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CHAIN, ++ MD_SV_CHAIN, ad->chain, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain fetched and saved"); ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* ACME driver init */ ++ ++static apr_status_t acme_driver_init(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad; ++ apr_status_t rv; ++ ++ 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); ++ ++ /* 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. */ ++ ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char *)); ++ if (d->challenge) { ++ /* we have been told to use this type */ ++ APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, d->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_TLSSNI01; ++ APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_HTTP01; ++ } ++ ++ if (!d->can_http && !d->can_https) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: the server seems neither " ++ "reachable via http (port 80) nor https (port 443). The ACME protocol " ++ "needs at least one of those so the CA can talk to the server and verify " ++ "a domain ownership.", d->md->name); ++ return APR_EGENERAL; ++ } ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: init driver", d->md->name); ++ ++ rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ad->md, d->p); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: checked stage md", d->md->name); ++ if (d->reset || APR_STATUS_IS_ENOENT(rv)) { ++ /* reset the staging area for this domain */ ++ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); ++ if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { ++ return rv; ++ } ++ rv = APR_SUCCESS; ++ } ++ ++ if (ad->md) { ++ /* staging in progress. ++ * There are certain md properties that are updated in staging, others are only ++ * updated in the domains store. Are these still the same? If not, we better ++ * start anew. ++ */ ++ if (strcmp(d->md->ca_url, ad->md->ca_url) ++ || strcmp(d->md->ca_proto, ad->md->ca_proto)) { ++ /* reject staging info in this case */ ++ ad->md = NULL; ++ return APR_SUCCESS; ++ } ++ ++ if (d->md->ca_agreement ++ && (!ad->md->ca_agreement || strcmp(d->md->ca_agreement, ad->md->ca_agreement))) { ++ ad->md->ca_agreement = d->md->ca_agreement; ++ } ++ ++ /* look for new ACME account information collected there */ ++ rv = md_reg_creds_get(&ad->ncreds, d->reg, MD_SG_STAGING, d->md, d->p); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: checked creds", d->md->name); ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ rv = APR_SUCCESS; ++ } ++ } ++ ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* ACME staging */ ++ ++static apr_status_t acme_stage(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv = APR_SUCCESS; ++ int renew = 1; ++ ++ if (md_log_is_level(d->p, MD_LOG_DEBUG)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " ++ "state=%d, can_http=%d, can_https=%d, challenges='%s'", ++ d->md->name, d->md->state, d->can_http, d->can_https, ++ apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); ++ } ++ ++ /* Find out where we're at with this managed domain */ ++ if (ad->ncreds && ad->ncreds->pkey && ad->ncreds->cert && ad->ncreds->chain) { ++ /* There is a full set staged, to be loaded */ ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: all data staged", d->md->name); ++ renew = 0; ++ } ++ ++ if (renew) { ++ if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, d->md->ca_url)) ++ || APR_SUCCESS != (rv = md_acme_setup(ad->acme))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, "%s: setup ACME(%s)", ++ d->md->name, d->md->ca_url); ++ return rv; ++ } ++ ++ if (!ad->md) { ++ /* re-initialize staging */ ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 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->cert_url = NULL; /* do not retrieve the old cert */ ++ rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: save staged md", ++ ad->md->name); ++ } ++ ++ if (APR_SUCCESS == rv && !ad->cert) { ++ md_cert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->cert, d->p); ++ } ++ ++ if (APR_SUCCESS == rv && !ad->cert) { ++ ad->phase = "get certificate"; ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: need certificate", d->md->name); ++ ++ /* Chose (or create) and ACME account to use */ ++ rv = ad_set_acct(d); ++ ++ /* Check that the account agreed to the terms-of-service, otherwise ++ * requests for new authorizations are denied. ToS may change during the ++ * lifetime of an account */ ++ if (APR_SUCCESS == rv) { ++ ad->phase = "check agreement"; ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: check Terms-of-Service agreement", d->md->name); ++ ++ rv = md_acme_check_agreement(ad->acme, d->p, ad->md->ca_agreement); ++ } ++ ++ /* If we know a cert's location, try to get it. Previous download might ++ * have failed. If server 404 it, we clear our memory of it. */ ++ if (APR_SUCCESS == rv && ad->md->cert_url) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: polling certificate", d->md->name); ++ rv = ad_cert_poll(d, 1); ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ /* Server reports to know nothing about it. */ ++ ad->md->cert_url = NULL; ++ rv = md_reg_update(d->reg, d->p, ad->md->name, ad->md, MD_UPD_CERT_URL); ++ } ++ } ++ ++ if (APR_SUCCESS == rv && !ad->cert) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: setup new authorization", d->md->name); ++ if (APR_SUCCESS != (rv = ad_setup_authz(d))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup authz resource", ++ ad->md->name); ++ goto out; ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: setup new challenges", d->md->name); ++ if (APR_SUCCESS != (rv = ad_start_challenges(d))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: start challenges", ++ ad->md->name); ++ goto out; ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: monitoring challenge status", d->md->name); ++ if (APR_SUCCESS != (rv = ad_monitor_challenges(d))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: monitor challenges", ++ ad->md->name); ++ goto out; ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: creating certificate request", d->md->name); ++ if (APR_SUCCESS != (rv = ad_setup_certificate(d))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup certificate", ++ ad->md->name); ++ goto out; ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: received certificate", d->md->name); ++ } ++ ++ } ++ ++ if (APR_SUCCESS == rv && !ad->chain) { ++ ad->phase = "install chain"; ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, ++ "%s: retrieving certificate chain", d->md->name); ++ rv = ad_chain_install(d); ++ } ++ } ++out: ++ return rv; ++} ++ ++static apr_status_t acme_driver_stage(md_proto_driver_t *d) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv; ++ ++ ad->phase = "ACME staging"; ++ if (APR_SUCCESS == (rv = acme_stage(d))) { ++ ad->phase = "staging done"; ++ } ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", ++ d->md->name, d->proto->protocol, ad->phase); ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* ACME preload */ ++ ++static apr_status_t acme_preload(md_store_t *store, md_store_group_t load_group, ++ const char *name, apr_pool_t *p) ++{ ++ apr_status_t rv; ++ md_pkey_t *pkey, *acct_key; ++ md_t *md; ++ md_cert_t *cert; ++ apr_array_header_t *chain; ++ struct md_acme_acct_t *acct; ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: preload start", name); ++ /* Load all data which will be taken into the DOMAIN storage 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 TMP is verified, we can swap/archive groups with a rename ++ * 5. Reading/Writing the data will apply/remove any group specific data encryption. ++ * With the exemption that DOMAINS and TMP must apply the same policy/keys. ++ */ ++ if (APR_SUCCESS != (rv = md_load(store, MD_SG_STAGING, name, &md, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading md json", name); ++ return rv; ++ } ++ if (APR_SUCCESS != (rv = md_cert_load(store, MD_SG_STAGING, name, &cert, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading certificate", name); ++ return rv; ++ } ++ if (APR_SUCCESS != (rv = md_chain_load(store, MD_SG_STAGING, name, &chain, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading cert chain", name); ++ return rv; ++ } ++ if (APR_SUCCESS != (rv = md_pkey_load(store, MD_SG_STAGING, name, &pkey, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading staging private key", name); ++ return rv; ++ } ++ ++ /* See if staging holds a new or modified account data */ ++ rv = md_acme_acct_load(&acct, &acct_key, store, MD_SG_STAGING, name, p); ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ acct = NULL; ++ acct_key = NULL; ++ rv = APR_SUCCESS; ++ } ++ else if (APR_SUCCESS != rv) { ++ return rv; ++ } ++ ++ /* Remove any authz information we have here or in MD_SG_CHALLENGES */ ++ md_acme_authz_set_purge(store, MD_SG_STAGING, p, name); ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "%s: staged data load, purging tmp space", name); ++ rv = md_store_purge(store, p, load_group, name); ++ if (APR_SUCCESS != rv) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error purging preload storage", name); ++ return rv; ++ } ++ ++ if (acct) { ++ md_acme_t *acme; ++ ++ if (APR_SUCCESS != (rv = md_acme_create(&acme, p, md->ca_url)) ++ || APR_SUCCESS != (rv = md_acme_acct_save(store, p, acme, acct, acct_key))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error saving acct", name); ++ return rv; ++ } ++ md->ca_account = acct->id; ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: saved ACME account %s", ++ name, acct->id); ++ } ++ ++ if (APR_SUCCESS != (rv = md_save(store, p, load_group, md, 1))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving md json", name); ++ return rv; ++ } ++ if (APR_SUCCESS != (rv = md_cert_save(store, p, load_group, name, cert, 1))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving certificate", name); ++ return rv; ++ } ++ if (APR_SUCCESS != (rv = md_chain_save(store, p, load_group, name, chain, 1))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving cert chain", name); ++ return rv; ++ } ++ if (APR_SUCCESS != (rv = md_pkey_save(store, p, load_group, name, pkey, 1))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving domain private key", name); ++ return rv; ++ } ++ ++ return rv; ++} ++ ++static apr_status_t acme_driver_preload(md_proto_driver_t *d, md_store_group_t group) ++{ ++ md_acme_driver_t *ad = d->baton; ++ apr_status_t rv; ++ ++ ad->phase = "ACME preload"; ++ if (APR_SUCCESS == (rv = acme_preload(d->store, group, d->md->name, d->p))) { ++ ad->phase = "preload done"; ++ } ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", ++ d->md->name, d->proto->protocol, ad->phase); ++ return rv; ++} ++ ++static md_proto_t ACME_PROTO = { ++ MD_PROTO_ACME, acme_driver_init, acme_driver_stage, acme_driver_preload ++}; ++ ++apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) ++{ ++ apr_hash_set(protos, MD_PROTO_ACME, sizeof(MD_PROTO_ACME)-1, &ACME_PROTO); ++ return APR_SUCCESS; ++} diff --cc modules/md/md_config.c index 0000000000,0000000000..5a31255372 new file mode 100644 --- /dev/null +++ b/modules/md/md_config.c @@@ -1,0 -1,0 +1,583 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include "md.h" ++#include "md_config.h" ++#include "md_util.h" ++ ++extern module AP_MODULE_DECLARE_DATA md_module; ++APLOG_USE_MODULE(md); ++ ++#define DEF_VAL (-1) ++ ++static md_config_t defconf = { ++ "default", ++ NULL, ++ 80, ++ 443, ++ NULL, ++ MD_ACME_DEF_URL, ++ "ACME", ++ NULL, ++ NULL, ++ MD_DRIVE_AUTO, ++ apr_time_from_sec(14 * MD_SECS_PER_DAY), ++ NULL, ++ "md", ++ NULL ++}; ++ ++#define CONF_S_NAME(s) (s && s->server_hostname? s->server_hostname : "default") ++ ++void *md_config_create_svr(apr_pool_t *pool, server_rec *s) ++{ ++ md_config_t *conf = (md_config_t *)apr_pcalloc(pool, sizeof(md_config_t)); ++ ++ conf->name = apr_pstrcat(pool, "srv[", CONF_S_NAME(s), "]", NULL); ++ conf->s = s; ++ conf->local_80 = DEF_VAL; ++ conf->local_443 = DEF_VAL; ++ conf->drive_mode = DEF_VAL; ++ conf->mds = apr_array_make(pool, 5, sizeof(const md_t *)); ++ conf->renew_window = DEF_VAL; ++ ++ return conf; ++} ++ ++static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) ++{ ++ md_config_t *base = (md_config_t *)basev; ++ md_config_t *add = (md_config_t *)addv; ++ md_config_t *n = (md_config_t *)apr_pcalloc(pool, sizeof(md_config_t)); ++ char *name = apr_pstrcat(pool, "[", CONF_S_NAME(add->s), ", ", CONF_S_NAME(base->s), "]", NULL); ++ md_t *md; ++ int i; ++ ++ n->name = name; ++ n->local_80 = (add->local_80 != DEF_VAL)? add->local_80 : base->local_80; ++ n->local_443 = (add->local_443 != DEF_VAL)? add->local_443 : base->local_443; ++ ++ /* I think we should not merge md definitions. They should reside where ++ * they were defined */ ++ n->mds = apr_array_make(pool, add->mds->nelts, sizeof(const md_t *)); ++ for (i = 0; i < add->mds->nelts; ++i) { ++ md = APR_ARRAY_IDX(add->mds, i, md_t*); ++ APR_ARRAY_PUSH(n->mds, md_t *) = md_clone(pool, md); ++ } ++ n->ca_url = add->ca_url? add->ca_url : base->ca_url; ++ n->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto; ++ n->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement; ++ n->drive_mode = (add->drive_mode != DEF_VAL)? add->drive_mode : base->drive_mode; ++ n->md = NULL; ++ n->base_dir = add->base_dir? add->base_dir : base->base_dir; ++ n->renew_window = (add->renew_window != DEF_VAL)? add->renew_window : base->renew_window; ++ n->ca_challenges = (add->ca_challenges? apr_array_copy(pool, add->ca_challenges) ++ : (base->ca_challenges? apr_array_copy(pool, base->ca_challenges) : NULL)); ++ return n; ++} ++ ++void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv) ++{ ++ return md_config_merge(pool, basev, addv); ++} ++ ++void *md_config_create_dir(apr_pool_t *pool, char *dummy) ++{ ++ md_config_dir_t *conf = apr_pcalloc(pool, sizeof(*conf)); ++ return conf; ++} ++ ++void *md_config_merge_dir(apr_pool_t *pool, void *basev, void *addv) ++{ ++ md_config_dir_t *base = basev; ++ md_config_dir_t *add = addv; ++ md_config_dir_t *n = apr_pcalloc(pool, sizeof(*n)); ++ n->md = add->md? add->md : base->md; ++ return n; ++} ++ ++static int inside_section(cmd_parms *cmd) { ++ return (cmd->directive->parent ++ && !ap_cstr_casecmp(cmd->directive->parent->directive, "pool, cmd->cmd->name, ++ " is only valid inside a directive->parent? cmd->directive->parent->directive : "root", ++ 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 *md_config_sec_start(cmd_parms *cmd, void *mconfig, const char *arg) ++{ ++ md_config_t *sconf = ap_get_module_config(cmd->server->module_config, &md_module); ++ const char *endp = ap_strrchr_c(arg, '>'); ++ ap_conf_vector_t *new_dir_conf = ap_create_per_dir_config(cmd->pool); ++ int old_overrides = cmd->override; ++ char *old_path = cmd->path; ++ const char *err, *name; ++ md_config_dir_t *dconf; ++ md_t *md; ++ ++ if (NULL != (err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE))) { ++ return err; ++ } ++ ++ if (endp == NULL) { ++ return apr_pstrcat(cmd->pool, cmd->cmd->name, "> directive missing closing '>'", NULL); ++ } ++ ++ arg = apr_pstrndup(cmd->pool, arg, endp-arg); ++ if (!arg || !*arg) { ++ return " block must specify a unique domain name"; ++ } ++ ++ cmd->path = ap_getword_white(cmd->pool, &arg); ++ name = cmd->path; ++ ++ md = md_create_empty(cmd->pool); ++ md->name = name; ++ APR_ARRAY_PUSH(md->domains, const char*) = name; ++ md->drive_mode = DEF_VAL; ++ ++ while (*arg != '\0') { ++ name = ap_getword_white(cmd->pool, &arg); ++ APR_ARRAY_PUSH(md->domains, const char*) = name; ++ } ++ ++ dconf = ap_set_config_vectors(cmd->server, new_dir_conf, cmd->path, &md_module, cmd->pool); ++ dconf->md = md; ++ ++ if (NULL == (err = ap_walk_config(cmd->directive->first_child, cmd, new_dir_conf))) { ++ APR_ARRAY_PUSH(sconf->mds, const md_t *) = md; ++ } ++ ++ cmd->path = old_path; ++ cmd->override = old_overrides; ++ ++ return err; ++} ++ ++static const char *md_config_sec_add_members(cmd_parms *cmd, void *dc, ++ int argc, char *const argv[]) ++{ ++ md_config_dir_t *dconfig = dc; ++ apr_array_header_t *domains; ++ const char *err; ++ int i; ++ ++ if (NULL != (err = md_section_check(cmd))) { ++ return err; ++ } ++ ++ domains = dconfig->md->domains; ++ for (i = 0; i < argc; ++i) { ++ add_domain_name(domains, argv[i], cmd->pool); ++ } ++ return NULL; ++} ++ ++static const char *md_config_set_names(cmd_parms *cmd, void *arg, ++ int argc, char *const argv[]) ++{ ++ md_config_t *config = (md_config_t *)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; ++ ++ err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE); ++ if (err) { ++ return err; ++ } ++ ++ for (i = 0; i < argc; ++i) { ++ add_domain_name(domains, argv[i], cmd->pool); ++ } ++ err = md_create(&md, cmd->pool, domains); ++ if (err) { ++ return err; ++ } ++ ++ if (cmd->config_file) { ++ md->defn_name = cmd->config_file->name; ++ md->defn_line_number = cmd->config_file->line_number; ++ } ++ ++ APR_ARRAY_PUSH(config->mds, md_t *) = md; ++ ++ return NULL; ++} ++ ++static const char *md_config_set_ca(cmd_parms *cmd, void *dc, const char *value) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err; ++ ++ if (inside_section(cmd)) { ++ md_config_dir_t *dconf = dc; ++ dconf->md->ca_url = value; ++ } ++ else { ++ if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { ++ return err; ++ } ++ config->ca_url = value; ++ } ++ return NULL; ++} ++ ++static const char *md_config_set_ca_proto(cmd_parms *cmd, void *dc, const char *value) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err; ++ ++ if (inside_section(cmd)) { ++ md_config_dir_t *dconf = dc; ++ dconf->md->ca_proto = value; ++ } ++ else { ++ if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { ++ return err; ++ } ++ config->ca_proto = value; ++ } ++ return NULL; ++} ++ ++static const char *md_config_set_agreement(cmd_parms *cmd, void *dc, const char *value) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err; ++ ++ if (inside_section(cmd)) { ++ md_config_dir_t *dconf = dc; ++ dconf->md->ca_agreement = value; ++ } ++ else { ++ if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { ++ return err; ++ } ++ config->ca_agreement = value; ++ } ++ return NULL; ++} ++ ++static const char *md_config_set_drive_mode(cmd_parms *cmd, void *dc, const char *value) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err; ++ md_drive_mode_t drive_mode; ++ ++ if (!apr_strnatcasecmp("auto", value) || !apr_strnatcasecmp("automatic", value)) { ++ drive_mode = MD_DRIVE_AUTO; ++ } ++ else if (!apr_strnatcasecmp("always", value)) { ++ drive_mode = MD_DRIVE_ALWAYS; ++ } ++ else if (!apr_strnatcasecmp("manual", value) || !apr_strnatcasecmp("stick", value)) { ++ drive_mode = MD_DRIVE_MANUAL; ++ } ++ else { ++ return apr_pstrcat(cmd->pool, "unknown MDDriveMode ", value, NULL); ++ } ++ ++ if (inside_section(cmd)) { ++ md_config_dir_t *dconf = dc; ++ dconf->md->drive_mode = drive_mode; ++ } ++ else { ++ if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { ++ return err; ++ } ++ config->drive_mode = drive_mode; ++ } ++ return NULL; ++} ++ ++static apr_status_t duration_parse(const char *value, apr_interval_time_t *ptimeout, ++ const char *def_unit) ++{ ++ char *endp; ++ long funits = 1; ++ apr_status_t rv; ++ apr_int64_t n; ++ ++ n = apr_strtoi64(value, &endp, 10); ++ if (errno) { ++ return errno; ++ } ++ if (!endp || !*endp) { ++ if (strcmp(def_unit, "d") == 0) { ++ def_unit = "s"; ++ funits = MD_SECS_PER_DAY; ++ } ++ } ++ else if (*endp == 'd') { ++ *ptimeout = apr_time_from_sec(n * MD_SECS_PER_DAY); ++ return APR_SUCCESS; ++ } ++ else { ++ def_unit = endp; ++ } ++ rv = ap_timeout_parameter_parse(value, ptimeout, def_unit); ++ if (APR_SUCCESS == rv && funits > 1) { ++ *ptimeout *= funits; ++ } ++ return rv; ++} ++ ++static const char *md_config_set_renew_window(cmd_parms *cmd, void *dc, const char *value) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err; ++ apr_interval_time_t timeout; ++ ++ /* Inspired by http_core.c */ ++ if (duration_parse(value, &timeout, "d") != APR_SUCCESS) { ++ return "MDRenewWindow has wrong format"; ++ } ++ ++ if (inside_section(cmd)) { ++ md_config_dir_t *dconf = dc; ++ dconf->md->renew_window = timeout; ++ } ++ else { ++ if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { ++ return err; ++ } ++ config->renew_window = timeout; ++ } ++ return NULL; ++} ++ ++static const char *md_config_set_store_dir(cmd_parms *cmd, void *arg, const char *value) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); ++ ++ if (err) { ++ return err; ++ } ++ config->base_dir = value; ++ (void)arg; ++ return NULL; ++} ++ ++static const char *set_port_map(md_config_t *config, const char *value) ++{ ++ int net_port, local_port; ++ char *endp; ++ ++ net_port = (int)apr_strtoi64(value, &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, &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: ++ config->local_80 = local_port; ++ break; ++ case 443: ++ config->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_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); ++ ++ (void)arg; ++ if (!err) { ++ err = set_port_map(config, v1); ++ } ++ if (!err && v2) { ++ err = set_port_map(config, v2); ++ } ++ return err; ++} ++ ++static const char *md_config_set_cha_tyes(cmd_parms *cmd, void *dc, ++ int argc, char *const argv[]) ++{ ++ md_config_t *config = (md_config_t *)md_config_get(cmd->server); ++ apr_array_header_t **pcha, *ca_challenges; ++ const char *err; ++ int i; ++ ++ if (inside_section(cmd)) { ++ md_config_dir_t *dconf = dc; ++ pcha = &dconf->md->ca_challenges; ++ } ++ else { ++ if (NULL != (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { ++ return err; ++ } ++ pcha = &config->ca_challenges; ++ } ++ ++ ca_challenges = *pcha; ++ if (!ca_challenges) { ++ *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; ++} ++ ++ ++#define AP_END_CMD AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) ++ ++const command_rec md_cmds[] = { ++ AP_INIT_RAW_ARGS("module_config, &md_module); ++ ap_assert(cfg); ++ if (cfg->s != s && p) { ++ cfg = md_config_merge(p, &defconf, cfg); ++ cfg->name = apr_pstrcat(p, CONF_S_NAME(s), cfg->name, NULL); ++ ap_set_module_config(s->module_config, &md_module, cfg); ++ } ++ return cfg; ++} ++ ++const md_config_t *md_config_get(server_rec *s) ++{ ++ return config_get_int(s, NULL); ++} ++ ++const md_config_t *md_config_get_unique(server_rec *s, apr_pool_t *p) ++{ ++ assert(p); ++ return config_get_int(s, p); ++} ++ ++const md_config_t *md_config_cget(conn_rec *c) ++{ ++ return md_config_get(c->base_server); ++} ++ ++const char *md_config_gets(const md_config_t *config, md_config_var_t var) ++{ ++ switch (var) { ++ case MD_CONFIG_CA_URL: ++ return config->ca_url? config->ca_url : defconf.ca_url; ++ case MD_CONFIG_CA_PROTO: ++ return config->ca_proto? config->ca_proto : defconf.ca_proto; ++ case MD_CONFIG_BASE_DIR: ++ return config->base_dir? config->base_dir : defconf.base_dir; ++ case MD_CONFIG_CA_AGREEMENT: ++ return config->ca_agreement? config->ca_agreement : defconf.ca_agreement; ++ default: ++ return NULL; ++ } ++} ++ ++int md_config_geti(const md_config_t *config, md_config_var_t var) ++{ ++ switch (var) { ++ case MD_CONFIG_DRIVE_MODE: ++ return (config->drive_mode != DEF_VAL)? config->drive_mode : defconf.drive_mode; ++ case MD_CONFIG_LOCAL_80: ++ return (config->local_80 != DEF_VAL)? config->local_80 : 80; ++ case MD_CONFIG_LOCAL_443: ++ return (config->local_443 != DEF_VAL)? config->local_443 : 443; ++ default: ++ return 0; ++ } ++} ++ ++apr_interval_time_t md_config_get_interval(const md_config_t *config, md_config_var_t var) ++{ ++ switch (var) { ++ case MD_CONFIG_RENEW_WINDOW: ++ return (config->renew_window != DEF_VAL)? config->renew_window : defconf.renew_window; ++ default: ++ return 0; ++ } ++} diff --cc modules/md/md_config.h index 0000000000,0000000000..3568f7c6ac new file mode 100644 --- /dev/null +++ b/modules/md/md_config.h @@@ -1,0 -1,0 +1,77 @@@ ++/* Copyright 2017 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 mod_md_md_config_h ++#define mod_md_md_config_h ++ ++struct md_store_t; ++ ++typedef enum { ++ MD_CONFIG_CA_URL, ++ MD_CONFIG_CA_PROTO, ++ MD_CONFIG_BASE_DIR, ++ MD_CONFIG_CA_AGREEMENT, ++ MD_CONFIG_DRIVE_MODE, ++ MD_CONFIG_LOCAL_80, ++ MD_CONFIG_LOCAL_443, ++ MD_CONFIG_RENEW_WINDOW, ++} md_config_var_t; ++ ++typedef struct { ++ const char *name; ++ const server_rec *s; ++ ++ int local_80; ++ int local_443; ++ ++ apr_array_header_t *mds; /* array of md_t pointers */ ++ const char *ca_url; ++ const char *ca_proto; ++ const char *ca_agreement; ++ apr_array_header_t *ca_challenges; /* challenge types allowed */ ++ ++ int drive_mode; ++ apr_interval_time_t renew_window; /* time for renewal before expiry */ ++ ++ const md_t *md; ++ const char *base_dir; ++ struct md_store_t *store; ++ ++} md_config_t; ++ ++typedef struct { ++ md_t *md; ++} md_config_dir_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); ++void *md_config_create_dir(apr_pool_t *pool, char *dummy); ++void *md_config_merge_dir(apr_pool_t *pool, void *basev, void *addv); ++ ++extern const command_rec md_cmds[]; ++ ++/* Get the effective md configuration for the connection */ ++const md_config_t *md_config_cget(conn_rec *c); ++/* Get the effective md configuration for the server */ ++const md_config_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 */ ++const md_config_t *md_config_get_unique(server_rec *s, apr_pool_t *p); ++ ++const char *md_config_gets(const md_config_t *config, md_config_var_t var); ++int md_config_geti(const md_config_t *config, md_config_var_t var); ++apr_interval_time_t md_config_get_interval(const md_config_t *config, md_config_var_t var); ++ ++#endif /* md_config_h */ diff --cc modules/md/md_core.c index 0000000000,0000000000..a65c639f07 new file mode 100644 --- /dev/null +++ b/modules/md/md_core.c @@@ -1,0 -1,0 +1,321 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include "md_json.h" ++#include "md.h" ++#include "md_log.h" ++#include "md_store.h" ++#include "md_util.h" ++ ++ ++int md_contains(const md_t *md, const char *domain) ++{ ++ return md_array_str_index(md->domains, domain, 0, 0) >= 0; ++} ++ ++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)) { ++ 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)) { ++ ++hits; ++ } ++ } ++ return hits; ++} ++ ++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->drive_mode = MD_DRIVE_DEFAULT; ++ md->defn_name = "unknown"; ++ md->defn_line_number = 0; ++ } ++ return md; ++} ++ ++int md_equal_domains(const md_t *md1, const md_t *md2) ++{ ++ 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)) { ++ 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)) { ++ return 0; ++ } ++ } ++ return 1; ++ } ++ return 0; ++} ++ ++md_t *md_find_closest_match(apr_array_header_t *mds, const md_t *md) ++{ ++ md_t *candidate, *m; ++ apr_size_t cand_n, n; ++ int i; ++ ++ candidate = md_get_by_name(mds, md->name); ++ if (!candidate) { ++ /* try to find an instance that contains all domain names from md */ ++ for (i = 0; i < mds->nelts; ++i) { ++ m = APR_ARRAY_IDX(mds, i, md_t *); ++ if (md_contains_domains(m, md)) { ++ return m; ++ } ++ } ++ /* no matching name and no md in the list has all domains. ++ * We consider that managed domain as closest match that contains at least one ++ * domain name from md, ONLY if there is no other one that also has. ++ */ ++ cand_n = 0; ++ for (i = 0; i < mds->nelts; ++i) { ++ m = APR_ARRAY_IDX(mds, i, md_t *); ++ n = md_common_name_count(md, m); ++ if (n > cand_n) { ++ candidate = m; ++ cand_n = n; ++ } ++ } ++ } ++ return candidate; ++} ++ ++md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name) ++{ ++ int i; ++ 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)) { ++ 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; ++} ++ ++const char *md_create(md_t **pmd, apr_pool_t *p, apr_array_header_t *domains) ++{ ++ md_t *md; ++ ++ if (domains->nelts <= 0) { ++ return "needs at least one domain name"; ++ } ++ ++ md = md_create_empty(p); ++ if (!md) { ++ return "not enough memory"; ++ } ++ ++ md->domains = md_array_str_compact(p, domains, 0); ++ md->name = APR_ARRAY_IDX(md->domains, 0, const char *); ++ ++ *pmd = md; ++ return NULL; ++} ++ ++/**************************************************************************************************/ ++/* 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); ++ } ++ } ++ 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->drive_mode = src->drive_mode; ++ md->domains = md_array_str_compact(p, src->domains, 0); ++ md->renew_window = src->renew_window; ++ md->contacts = md_array_str_clone(p, src->contacts); ++ if (src->ca_url) md->ca_url = apr_pstrdup(p, src->ca_url); ++ if (src->ca_proto) md->ca_proto = apr_pstrdup(p, src->ca_proto); ++ 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); ++ if (src->cert_url) md->cert_url = apr_pstrdup(p, src->cert_url); ++ md->defn_line_number = src->defn_line_number; ++ if (src->ca_challenges) { ++ md->ca_challenges = md_array_str_clone(p, src->ca_challenges); ++ } ++ } ++ 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_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_url, json, MD_KEY_CA, MD_KEY_URL, NULL); ++ md_json_sets(md->ca_agreement, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); ++ if (md->cert_url) { ++ md_json_sets(md->cert_url, json, MD_KEY_CERT, MD_KEY_URL, NULL); ++ } ++ md_json_setl(md->state, json, MD_KEY_STATE, NULL); ++ md_json_setl(md->drive_mode, json, MD_KEY_DRIVE_MODE, NULL); ++ if (md->expires > 0) { ++ char *ts = apr_pcalloc(p, APR_RFC822_DATE_LEN); ++ apr_rfc822_date(ts, md->expires); ++ md_json_sets(ts, json, MD_KEY_CERT, MD_KEY_EXPIRES, NULL); ++ } ++ md_json_setl(apr_time_sec(md->renew_window), json, MD_KEY_RENEW_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); ++ } ++ 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_url = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL); ++ md->ca_agreement = md_json_dups(p, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); ++ md->cert_url = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_URL, NULL); ++ md->state = (int)md_json_getl(json, MD_KEY_STATE, NULL); ++ md->drive_mode = (int)md_json_getl(json, MD_KEY_DRIVE_MODE, NULL); ++ md->domains = md_array_str_compact(p, md->domains, 0); ++ s = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_EXPIRES, NULL); ++ if (s && *s) { ++ md->expires = apr_date_parse_rfc(s); ++ } ++ md->renew_window = apr_time_from_sec(md_json_getl(json, MD_KEY_RENEW_WINDOW, NULL)); ++ 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); ++ } ++ return md; ++ } ++ return NULL; ++} ++ diff --cc modules/md/md_crypt.c index 0000000000,0000000000..92b1f0c803 new file mode 100644 --- /dev/null +++ b/modules/md/md_crypt.c @@@ -1,0 -1,0 +1,1128 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "md.h" ++#include "md_crypt.h" ++#include "md_log.h" ++#include "md_http.h" ++#include "md_util.h" ++ ++/* getpid for *NIX */ ++#if APR_HAVE_SYS_TYPES_H ++#include ++#endif ++#if APR_HAVE_UNISTD_H ++#include ++#endif ++ ++/* getpid for Windows */ ++#if APR_HAVE_PROCESS_H ++#include ++#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]; ++ 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 */ ++ apr_size_t n, l; ++ 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; ++ ++ l = sizeof(my_seed); ++ RAND_seed((unsigned char *)&my_seed, l); ++ ++ /* ++ * seed in some current state of the run-time stack (128 bytes) ++ */ ++#if HAVE_VALGRIND && 0 ++ if (ssl_running_on_valgrind) { ++ VALGRIND_MAKE_MEM_DEFINED(stackdata, sizeof(stackdata)); ++ } ++#endif ++ 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; ++} ++ ++typedef struct { ++ char *data; ++ apr_size_t len; ++} buffer; ++ ++static apr_status_t fwrite_buffer(void *baton, apr_file_t *f, apr_pool_t *p) ++{ ++ buffer *buf = baton; ++ return apr_file_write_full(f, buf->data, buf->len, &buf->len); ++} ++ ++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; ++ if (ctx->pass_len > 0) { ++ if (ctx->pass_len < size) { ++ size = (int)ctx->pass_len; ++ } ++ memcpy(buf, ctx->pass_phrase, size); ++ } ++ return ctx->pass_len; ++} ++ ++/**************************************************************************************************/ ++/* private keys */ ++ ++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 { ++ 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(buffer *buffer, 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; ++ passwd_ctx ctx; ++ unsigned long err; ++ ++ if (!bio) { ++ return APR_ENOMEM; ++ } ++ if (pass_len > INT_MAX) { ++ return APR_EINVAL; ++ } ++ 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) { ++ return APR_ENOTIMPL; ++ } ++ } ++ ++ ERR_clear_error(); ++ if (!PEM_write_bio_PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) { ++ BIO_free(bio); ++ 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)); ++ return APR_EINVAL; ++ } ++ ++ buffer->len = BIO_pending(bio); ++ if (buffer->len > 0) { ++ buffer->data = apr_palloc(p, buffer->len+1); ++ buffer->len = BIO_read(bio, buffer->data, (int)buffer->len); ++ buffer->data[buffer->len] = '\0'; ++ } ++ BIO_free(bio); ++ return APR_SUCCESS; ++} ++ ++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) ++{ ++ buffer 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_gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, 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, bits) >= 0 ++ && EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) >= 0) { ++ rv = APR_SUCCESS; ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "unable to generate new key"); ++ *ppkey = NULL; ++ rv = APR_EGENERAL; ++ } ++ ++ if (ctx != NULL) { ++ EVP_PKEY_CTX_free(ctx); ++ } ++ return rv; ++} ++ ++#if OPENSSL_VERSION_NUMBER < 0x10100000L ++ ++#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) { ++ int len = BN_num_bytes(b); ++ char *buffer = apr_pcalloc(p, len); ++ if (buffer) { ++ BN_bn2bin(b, (unsigned char *)buffer); ++ return md_util_base64url_encode(buffer, len, p); ++ } ++ } ++ 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; ++ char *buffer; ++ unsigned int blen; ++ const char *sign64 = NULL; ++ apr_status_t rv = APR_ENOMEM; ++ ++ buffer = apr_pcalloc(p, EVP_PKEY_size(pkey->pkey)); ++ if (buffer) { ++ ctx = EVP_MD_CTX_create(); ++ if (ctx) { ++ rv = APR_ENOTIMPL; ++ if (EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) { ++ rv = APR_EGENERAL; ++ if (EVP_SignUpdate(ctx, d, dlen)) { ++ if (EVP_SignFinal(ctx, (unsigned char*)buffer, &blen, pkey->pkey)) { ++ sign64 = md_util_base64url_encode(buffer, blen, p); ++ if (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(unsigned char **pdigest, size_t *pdigest_len, ++ apr_pool_t *p, const char *d, size_t dlen) ++{ ++ EVP_MD_CTX *ctx = NULL; ++ unsigned char *buffer; ++ apr_status_t rv = APR_ENOMEM; ++ unsigned int blen; ++ ++ buffer = apr_pcalloc(p, EVP_MAX_MD_SIZE); ++ if (buffer) { ++ 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, d, dlen)) { ++ if (EVP_DigestFinal(ctx, buffer, &blen)) { ++ rv = APR_SUCCESS; ++ } ++ } ++ } ++ } ++ ++ if (ctx) { ++ EVP_MD_CTX_destroy(ctx); ++ } ++ } ++ ++ if (APR_SUCCESS == rv) { ++ *pdigest = buffer; ++ *pdigest_len = blen; ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "digest"); ++ *pdigest = NULL; ++ *pdigest_len = 0; ++ } ++ return rv; ++} ++ ++apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, ++ const char *d, size_t dlen) ++{ ++ const char *digest64 = NULL; ++ unsigned char *buffer; ++ size_t blen; ++ apr_status_t rv; ++ ++ if (APR_SUCCESS == (rv = sha256_digest(&buffer, &blen, p, d, dlen))) { ++ if (NULL == (digest64 = md_util_base64url_encode((const char*)buffer, blen, p))) { ++ rv = APR_EGENERAL; ++ } ++ } ++ *pdigest64 = digest64; ++ return rv; ++} ++ ++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_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, ++ const char *d, size_t dlen) ++{ ++ char *dhex = NULL, *cp; ++ const char * x; ++ unsigned char *buffer; ++ size_t blen; ++ apr_status_t rv; ++ int i; ++ ++ if (APR_SUCCESS == (rv = sha256_digest(&buffer, &blen, p, d, dlen))) { ++ cp = dhex = apr_pcalloc(p, 2 * blen + 1); ++ if (!dhex) { ++ rv = APR_EGENERAL; ++ } ++ for (i = 0; i < blen; ++i, cp += 2) { ++ x = hex_const[buffer[i]]; ++ cp[0] = x[0]; ++ cp[1] = x[1]; ++ } ++ } ++ *pdigesthex = dhex; ++ 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; ++} ++ ++static md_cert_t *make_cert(apr_pool_t *p, X509 *x509) ++{ ++ md_cert_t *cert = apr_pcalloc(p, sizeof(*cert)); ++ cert->pool = p; ++ cert->x509 = x509; ++ apr_pool_cleanup_register(p, cert, cert_cleanup, apr_pool_cleanup_null); ++ ++ return cert; ++} ++ ++void md_cert_free(md_cert_t *cert) ++{ ++ cert_cleanup(cert); ++} ++ ++void *md_cert_get_X509(struct md_cert_t *cert) ++{ ++ return cert->x509; ++} ++ ++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(md_cert_t *cert) ++{ ++ int secs, days; ++ apr_time_t time = apr_time_now(); ++ ASN1_TIME *not_after = X509_get_notAfter(cert->x509); ++ ++ if (ASN1_TIME_diff(&days, &secs, NULL, not_after)) { ++ time += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs); ++ } ++ return time; ++} ++ ++int md_cert_covers_domain(md_cert_t *cert, const char *domain_name) ++{ ++ if (!cert->alt_names) { ++ md_cert_get_alt_names(&cert->alt_names, cert, cert->pool); ++ } ++ if (cert->alt_names) { ++ return md_array_str_index(cert->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_array_str_index(cert->alt_names, name, 0, 0) < 0) { ++ 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, md_cert_t *cert, apr_pool_t *p) ++{ ++ int i, ext_idx, nid = NID_info_access; ++ X509_EXTENSION *ext; ++ X509V3_EXT_METHOD *ext_cls; ++ void *ext_data; ++ const char *uri = NULL; ++ apr_status_t rv = APR_ENOENT; ++ ++ /* Waddle through x509 API history to get someone that may be able ++ * to hand us the issuer url for the cert chain */ ++ ext_idx = X509_get_ext_by_NID(cert->x509, nid, -1); ++ ext = (ext_idx >= 0)? X509_get_ext(cert->x509, ext_idx) : NULL; ++ ext_cls = ext? (X509V3_EXT_METHOD*)X509V3_EXT_get(ext) : NULL; ++ if (ext_cls && (ext_data = X509_get_ext_d2i(cert->x509, nid, 0, 0))) { ++ CONF_VALUE *cval; ++ STACK_OF(CONF_VALUE) *ext_vals = ext_cls->i2v(ext_cls, ext_data, 0); ++ ++ for (i = 0; i < sk_CONF_VALUE_num(ext_vals); ++i) { ++ cval = sk_CONF_VALUE_value(ext_vals, i); ++ if (!strcmp("CA Issuers - URI", cval->name)) { ++ uri = apr_pstrdup(p, cval->value); ++ rv = APR_SUCCESS; ++ break; ++ } ++ } ++ } ++ *puri = (APR_SUCCESS == rv)? uri : NULL; ++ return rv; ++} ++ ++apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, 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 = (GENERAL_NAMES*)X509_get_ext_d2i(cert->x509, NID_subject_alt_name, NULL, NULL); ++ if (xalt_names) { ++ GENERAL_NAME *cval; ++ ++ 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: ++ case GEN_IPADD: ++ ASN1_STRING_to_UTF8(&buf, cval->d.ia5); ++ APR_ARRAY_PUSH(names, const char *) = apr_pstrdup(p, (char*)buf); ++ OPENSSL_free(buf); ++ break; ++ default: ++ break; ++ } ++ } ++ 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 = make_cert(p, x509); ++ } ++ else { ++ rv = APR_EINVAL; ++ } ++ } ++ ++ *pcert = (APR_SUCCESS == rv)? cert : NULL; ++ return rv; ++} ++ ++static apr_status_t cert_to_buffer(buffer *buffer, md_cert_t *cert, apr_pool_t *p) ++{ ++ BIO *bio = BIO_new(BIO_s_mem()); ++ ++ 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; ++ } ++ ++ buffer->len = BIO_pending(bio); ++ if (buffer->len > 0) { ++ buffer->data = apr_palloc(p, buffer->len+1); ++ buffer->len = BIO_read(bio, buffer->data, (int)buffer->len); ++ buffer->data[buffer->len] = '\0'; ++ } ++ 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) ++{ ++ buffer buffer; ++ apr_status_t rv; ++ ++ 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, md_cert_t *cert, apr_pool_t *p) ++{ ++ buffer buffer; ++ apr_status_t rv; ++ ++ if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) { ++ *ps64 = md_util_base64url_encode(buffer.data, buffer.len, p); ++ return APR_SUCCESS; ++ } ++ *ps64 = NULL; ++ 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; ++ apr_size_t der_len; ++ apr_status_t rv; ++ ++ ct = apr_table_get(res->headers, "Content-Type"); ++ if (!res->body || !ct || strcmp("application/pkix-cert", ct)) { ++ return APR_ENOENT; ++ } ++ ++ if (APR_SUCCESS == (rv = apr_brigade_length(res->body, 1, &data_len))) { ++ char *der; ++ 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, p))) { ++ const unsigned char *bf = (const unsigned char*)der; ++ X509 *x509; ++ ++ if (NULL == (x509 = d2i_X509(NULL, &bf, der_len))) { ++ rv = APR_EINVAL; ++ } ++ else { ++ *pcert = make_cert(p, x509); ++ rv = APR_SUCCESS; ++ } ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "cert parsed"); ++ } ++ return rv; ++} ++ ++md_cert_state_t md_cert_state_get(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_fload(apr_array_header_t **pcerts, apr_pool_t *p, const char *fname) ++{ ++ FILE *f; ++ apr_status_t rv; ++ apr_array_header_t *certs = NULL; ++ X509 *x509; ++ md_cert_t *cert; ++ unsigned long err; ++ ++ rv = md_util_fopen(&f, fname, "r"); ++ if (rv == APR_SUCCESS) { ++ certs = apr_array_make(p, 5, sizeof(md_cert_t *)); ++ ++ ERR_clear_error(); ++ while (NULL != (x509 = PEM_read_X509(f, NULL, NULL, NULL))) { ++ cert = make_cert(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); ++ *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; ++ ++ 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_name(const char *domain, apr_pool_t *p) ++{ ++ return apr_psprintf(p, "DNS:%s", domain); ++} ++ ++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; ++ ++ 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))) { ++ return APR_EGENERAL; ++ } ++ ++ ERR_clear_error(); ++ rv = X509_add_ext(x, ext, -1)? APR_SUCCESS : APR_EINVAL; ++ if (APR_SUCCESS != rv) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext nid=%dd value='%s'", ++ nid, value); ++ ++ } ++ 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; ++} ++ ++static apr_status_t add_must_staple(STACK_OF(X509_EXTENSION) *exts, const md_t *md, apr_pool_t *p) ++{ ++ ++ if (md->must_staple) { ++ X509_EXTENSION *x = X509V3_EXT_conf_nid(NULL, NULL, ++ NID_tlsfeature, (char*)"DER:30:03:02:01:05"); ++ if (NULL == x) { ++ 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 md_t *md, ++ md_pkey_t *pkey, apr_pool_t *p) ++{ ++ const char *s, *csr_der, *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; ++ int csr_der_len; ++ ++ assert(md->domains->nelts > 0); ++ ++ 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", md->name); ++ goto out; ++ } ++ ++ /* subject name == first domain */ ++ domain = APR_ARRAY_IDX(md->domains, 0, const unsigned char *); ++ if (!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", md->name); ++ rv = APR_EGENERAL; goto out; ++ } ++ /* collect extensions, such as alt names and must staple */ ++ if (APR_SUCCESS != (rv = sk_add_alt_names(exts, md->domains, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: collecting alt names", md->name); ++ rv = APR_EGENERAL; goto out; ++ } ++ if (APR_SUCCESS != (rv = add_must_staple(exts, md, p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: must staple", md->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", md->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", md->name); ++ rv = APR_EGENERAL; goto out; ++ } ++ /* sign, der encode and base64url encode */ ++ if (!X509_REQ_sign(csr, pkey->pkey, EVP_sha256())) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign csr", md->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", md->name); ++ rv = APR_EGENERAL; goto out; ++ } ++ s = csr_der = 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", md->name); ++ rv = APR_EGENERAL; goto out; ++ } ++ csr_der_64 = md_util_base64url_encode(csr_der, csr_der_len, 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; ++} ++ ++apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, ++ const char *domain, md_pkey_t *pkey, ++ apr_interval_time_t valid_for, apr_pool_t *p) ++{ ++ X509 *x; ++ X509_NAME *n = NULL; ++ md_cert_t *cert = NULL; ++ apr_status_t rv; ++ int days; ++ BIGNUM *big_rnd = NULL; ++ ASN1_INTEGER *asn1_rnd = NULL; ++ unsigned char rnd[20]; ++ ++ assert(domain); ++ ++ 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", domain); ++ 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", domain); ++ 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", domain); ++ rv = APR_EGENERAL; goto out; ++ } ++ /* set common name and issue */ ++ 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", domain); ++ rv = APR_EGENERAL; goto out; ++ } ++ /* cert are uncontrained (but not very trustworthy) */ ++ if (APR_SUCCESS != (rv = add_ext(x, NID_basic_constraints, "CA:TRUE, pathlen:0", p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set basic constraints ext", domain); ++ goto out; ++ } ++ /* add the domain as alt name */ ++ if (APR_SUCCESS != (rv = add_ext(x, NID_subject_alt_name, alt_name(domain, p), p))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", domain); ++ 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", domain); ++ rv = APR_EGENERAL; goto out; ++ } ++ ++ days = ((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; ++ } ++ ++ /* sign with same key */ ++ if (!X509_sign(x, pkey->pkey, EVP_sha256())) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", domain); ++ rv = APR_EGENERAL; goto out; ++ } ++ ++ cert = make_cert(p, x); ++ rv = APR_SUCCESS; ++ ++out: ++ if (!cert && x) { ++ X509_free(x); ++ } ++ if (n) { ++ X509_NAME_free(n); ++ } ++ if (big_rnd) { ++ BN_free(big_rnd); ++ } ++ if (asn1_rnd) { ++ ASN1_INTEGER_free(asn1_rnd); ++ } ++ *pcert = (APR_SUCCESS == rv)? cert : NULL; ++ return rv; ++} ++ diff --cc modules/md/md_crypt.h index 0000000000,0000000000..6f5c98826c new file mode 100644 --- /dev/null +++ b/modules/md/md_crypt.h @@@ -1,0 -1,0 +1,110 @@@ ++/* Copyright 2017 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 mod_md_md_crypt_h ++#define mod_md_md_crypt_h ++ ++#include ++ ++struct apr_array_header_t; ++struct md_t; ++struct md_http_response_t; ++struct md_cert_t; ++struct md_pkey_t; ++ ++/**************************************************************************************************/ ++/* random */ ++ ++apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p); ++ ++/**************************************************************************************************/ ++/* digests */ ++apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, ++ const char *d, size_t dlen); ++apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, ++ const char *d, size_t dlen); ++ ++/**************************************************************************************************/ ++/* private keys */ ++ ++typedef struct md_pkey_t md_pkey_t; ++ ++apr_status_t md_crypt_init(apr_pool_t *pool); ++ ++apr_status_t md_pkey_gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, int bits); ++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_cert_get_X509(struct md_cert_t *cert); ++void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey); ++ ++/**************************************************************************************************/ ++/* 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; ++ ++void md_cert_free(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); ++ ++apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *pool, ++ const struct md_http_response_t *res); ++ ++md_cert_state_t md_cert_state_get(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); ++apr_time_t md_cert_get_not_after(md_cert_t *cert); ++ ++apr_status_t md_cert_get_issuers_uri(const char **puri, md_cert_t *cert, apr_pool_t *p); ++apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, md_cert_t *cert, apr_pool_t *p); ++ ++apr_status_t md_cert_to_base64url(const char **ps64, 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_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_cert_req_create(const char **pcsr_der_64, const struct md_t *md, ++ md_pkey_t *pkey, apr_pool_t *p); ++ ++apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, ++ const char *domain, md_pkey_t *pkey, ++ apr_interval_time_t valid_for, apr_pool_t *p); ++ ++#endif /* md_crypt_h */ diff --cc modules/md/md_curl.c index 0000000000,0000000000..1eb5e554c0 new file mode 100644 --- /dev/null +++ b/modules/md/md_curl.c @@@ -1,0 -1,0 +1,301 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++ ++#include ++ ++#include ++#include ++#include ++ ++#include "md_http.h" ++#include "md_log.h" ++#include "md_curl.h" ++ ++/**************************************************************************************************/ ++/* md_http curl implementation */ ++ ++ ++static apr_status_t curl_status(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; ++ } ++} ++ ++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; ++ 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(data, bdata, blen); ++ read_len += blen; ++ max_len -= 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_http_response_t *res = baton; ++ 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 + len > 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_http_response_t *res = baton; ++ size_t len, clen = elen * nmemb; ++ const char *name = NULL, *value = "", *b = buffer; ++ int 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; ++} ++ ++static apr_status_t curl_init(md_http_request_t *req) ++{ ++ CURL *curl = curl_easy_init(); ++ if (!curl) { ++ return APR_EGENERAL; ++ } ++ ++ curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb); ++ curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL); ++ curl_easy_setopt(curl, CURLOPT_READFUNCTION, req_data_cb); ++ curl_easy_setopt(curl, CURLOPT_READDATA, NULL); ++ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, resp_data_cb); ++ curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL); ++ ++ req->internals = curl; ++ return APR_SUCCESS; ++} ++ ++typedef struct { ++ md_http_request_t *req; ++ struct curl_slist *hdrs; ++ 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; ++} ++ ++static apr_status_t curl_perform(md_http_request_t *req) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ int curle; ++ md_http_response_t *res; ++ CURL *curl; ++ struct curl_slist *req_hdrs = NULL; ++ ++ rv = curl_init(req); ++ curl = req->internals; ++ ++ res = apr_pcalloc(req->pool, sizeof(*res)); ++ ++ res->req = req; ++ res->rv = APR_SUCCESS; ++ res->status = 400; ++ res->headers = apr_table_make(req->pool, 5); ++ res->body = apr_brigade_create(req->pool, req->bucket_alloc); ++ ++ curl_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, res); ++ curl_easy_setopt(curl, CURLOPT_READDATA, req->body); ++ curl_easy_setopt(curl, CURLOPT_WRITEDATA, res); ++ ++ if (req->user_agent) { ++ curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent); ++ } ++ 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); ++ req_hdrs = ctx.hdrs; ++ if (ctx.rv == APR_SUCCESS) { ++ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_hdrs); ++ } ++ } ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, ++ "request %ld --> %s %s", req->id, req->method, req->url); ++ ++ if (md_log_is_level(req->pool, MD_LOG_TRACE3)) { ++ curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); ++ } ++ ++ curle = curl_easy_perform(curl); ++ res->rv = curl_status(curle); ++ ++ if (APR_SUCCESS == res->rv) { ++ long l; ++ res->rv = curl_status(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &l)); ++ if (APR_SUCCESS == res->rv) { ++ res->status = (int)l; ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, res->rv, req->pool, ++ "request %ld <-- %d", req->id, res->status); ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, res->rv, req->pool, ++ "request %ld failed(%d): %s", req->id, curle, curl_easy_strerror(curle)); ++ } ++ ++ if (req->cb) { ++ res->rv = req->cb(res); ++ } ++ ++ rv = res->rv; ++ md_http_req_destroy(req); ++ if (req_hdrs) { ++ curl_slist_free_all(req_hdrs); ++ } ++ ++ 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 curl_req_cleanup(md_http_request_t *req) ++{ ++ if (req->internals) { ++ curl_easy_cleanup(req->internals); ++ req->internals = NULL; ++ } ++} ++ ++static md_http_impl_t impl = { ++ md_curl_init, ++ curl_req_cleanup, ++ curl_perform ++}; ++ ++md_http_impl_t * md_curl_get_impl(apr_pool_t *p) ++{ ++ /* trigger early global curl init, before we are down a rabbit hole */ ++ md_curl_init(); ++ return &impl; ++} diff --cc modules/md/md_curl.h index 0000000000,0000000000..5b358cbdd9 new file mode 100644 --- /dev/null +++ b/modules/md/md_curl.h @@@ -1,0 -1,0 +1,23 @@@ ++/* Copyright 2017 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_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 --cc modules/md/md_http.c index 0000000000,0000000000..984066d9e1 new file mode 100644 --- /dev/null +++ b/modules/md/md_http.c @@@ -1,0 -1,0 +1,237 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++ ++#include ++#include ++#include ++ ++#include "md_http.h" ++#include "md_log.h" ++ ++struct md_http_t { ++ apr_pool_t *pool; ++ apr_bucket_alloc_t *bucket_alloc; ++ apr_off_t resp_limit; ++ md_http_impl_t *impl; ++ const char *user_agent; ++}; ++ ++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 long next_req_id; ++ ++apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent) ++{ ++ 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->bucket_alloc = apr_bucket_alloc_create(p); ++ if (!http->bucket_alloc) { ++ return APR_EGENERAL; ++ } ++ *phttp = http; ++ return APR_SUCCESS; ++} ++ ++void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit) ++{ ++ http->resp_limit = resp_limit; ++} ++ ++static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, ++ const char *method, const char *url, struct apr_table_t *headers, ++ md_http_cb *cb, void *baton) ++{ ++ 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; ++ } ++ ++ req = apr_pcalloc(pool, sizeof(*req)); ++ req->id = next_req_id++; ++ req->pool = pool; ++ req->bucket_alloc = http->bucket_alloc; ++ req->http = http; ++ req->method = method; ++ req->url = url; ++ req->headers = headers? apr_table_copy(req->pool, headers) : apr_table_make(req->pool, 5); ++ req->resp_limit = http->resp_limit; ++ req->cb = cb; ++ req->baton = baton; ++ req->user_agent = http->user_agent; ++ ++ *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); ++} ++ ++static apr_status_t schedule(md_http_request_t *req, ++ apr_bucket_brigade *body, int detect_clen, ++ long *preq_id) ++{ ++ apr_status_t rv; ++ ++ req->body = body; ++ req->body_len = body? -1 : 0; ++ ++ if (req->body && detect_clen) { ++ rv = apr_brigade_length(req->body, 1, &req->body_len); ++ if (rv != APR_SUCCESS) { ++ md_http_req_destroy(req); ++ return rv; ++ } ++ } ++ ++ if (req->body_len == 0 && apr_strnatcasecmp("GET", req->method)) { ++ apr_table_setn(req->headers, "Content-Length", "0"); ++ } ++ else if (req->body_len > 0) { ++ apr_table_setn(req->headers, "Content-Length", apr_off_t_toa(req->pool, req->body_len)); ++ } ++ ++ if (preq_id) { ++ *preq_id = req->id; ++ } ++ ++ /* we send right away */ ++ rv = req->http->impl->perform(req); ++ ++ return rv; ++} ++ ++apr_status_t md_http_GET(struct md_http_t *http, ++ const char *url, struct apr_table_t *headers, ++ md_http_cb *cb, void *baton, long *preq_id) ++{ ++ md_http_request_t *req; ++ apr_status_t rv; ++ ++ rv = req_create(&req, http, "GET", url, headers, cb, baton); ++ if (rv != APR_SUCCESS) { ++ return rv; ++ } ++ ++ return schedule(req, NULL, 0, preq_id); ++} ++ ++apr_status_t md_http_HEAD(struct md_http_t *http, ++ const char *url, struct apr_table_t *headers, ++ md_http_cb *cb, void *baton, long *preq_id) ++{ ++ md_http_request_t *req; ++ apr_status_t rv; ++ ++ rv = req_create(&req, http, "HEAD", url, headers, cb, baton); ++ if (rv != APR_SUCCESS) { ++ return rv; ++ } ++ ++ return schedule(req, NULL, 0, preq_id); ++} ++ ++apr_status_t md_http_POST(struct md_http_t *http, const char *url, ++ struct apr_table_t *headers, const char *content_type, ++ apr_bucket_brigade *body, ++ md_http_cb *cb, void *baton, long *preq_id) ++{ ++ md_http_request_t *req; ++ apr_status_t rv; ++ ++ rv = req_create(&req, http, "POST", url, headers, cb, baton); ++ if (rv != APR_SUCCESS) { ++ return rv; ++ } ++ ++ if (content_type) { ++ apr_table_set(req->headers, "Content-Type", content_type); ++ } ++ return schedule(req, body, 1, preq_id); ++} ++ ++apr_status_t md_http_POSTd(md_http_t *http, const char *url, ++ struct apr_table_t *headers, const char *content_type, ++ const char *data, size_t data_len, ++ md_http_cb *cb, void *baton, long *preq_id) ++{ ++ md_http_request_t *req; ++ apr_status_t rv; ++ apr_bucket_brigade *body = NULL; ++ ++ rv = req_create(&req, http, "POST", url, headers, cb, baton); ++ if (rv != APR_SUCCESS) { ++ return rv; ++ } ++ ++ if (data && data_len > 0) { ++ body = apr_brigade_create(req->pool, req->http->bucket_alloc); ++ rv = apr_brigade_write(body, NULL, NULL, data, data_len); ++ if (rv != APR_SUCCESS) { ++ md_http_req_destroy(req); ++ return rv; ++ } ++ } ++ ++ if (content_type) { ++ apr_table_set(req->headers, "Content-Type", content_type); ++ } ++ ++ return schedule(req, body, 1, preq_id); ++} ++ ++apr_status_t md_http_await(md_http_t *http, long req_id) ++{ ++ return APR_SUCCESS; ++} ++ diff --cc modules/md/md_http.h index 0000000000,0000000000..e6810b9da7 new file mode 100644 --- /dev/null +++ b/modules/md/md_http.h @@@ -1,0 -1,0 +1,99 @@@ ++/* Copyright 2017 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 mod_md_md_http_h ++#define mod_md_md_http_h ++ ++struct apr_table_t; ++struct apr_bucket_brigade; ++struct apr_bucket_alloc_t; ++ ++typedef struct md_http_t md_http_t; ++ ++typedef struct md_http_request_t md_http_request_t; ++typedef struct md_http_response_t md_http_response_t; ++ ++typedef apr_status_t md_http_cb(const md_http_response_t *res); ++ ++struct md_http_request_t { ++ long id; ++ md_http_t *http; ++ apr_pool_t *pool; ++ struct apr_bucket_alloc_t *bucket_alloc; ++ const char *method; ++ const char *url; ++ const char *user_agent; ++ apr_table_t *headers; ++ struct apr_bucket_brigade *body; ++ apr_off_t body_len; ++ apr_off_t resp_limit; ++ md_http_cb *cb; ++ void *baton; ++ void *internals; ++}; ++ ++struct md_http_response_t { ++ md_http_request_t *req; ++ apr_status_t rv; ++ int status; ++ apr_table_t *headers; ++ struct apr_bucket_brigade *body; ++}; ++ ++apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent); ++ ++void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit); ++ ++apr_status_t md_http_GET(md_http_t *http, ++ const char *url, struct apr_table_t *headers, ++ md_http_cb *cb, void *baton, long *preq_id); ++ ++apr_status_t md_http_HEAD(md_http_t *http, ++ const char *url, struct apr_table_t *headers, ++ md_http_cb *cb, void *baton, long *preq_id); ++ ++apr_status_t md_http_POST(md_http_t *http, const char *url, ++ struct apr_table_t *headers, const char *content_type, ++ struct apr_bucket_brigade *body, ++ md_http_cb *cb, void *baton, long *preq_id); ++ ++apr_status_t md_http_POSTd(md_http_t *http, const char *url, ++ struct apr_table_t *headers, const char *content_type, ++ const char *data, size_t data_len, ++ md_http_cb *cb, void *baton, long *preq_id); ++ ++apr_status_t md_http_await(md_http_t *http, long req_id); ++ ++void md_http_req_destroy(md_http_request_t *req); ++ ++/**************************************************************************************************/ ++/* interface to implementation */ ++ ++typedef apr_status_t md_http_init_cb(void); ++typedef void md_http_req_cleanup_cb(md_http_request_t *req); ++typedef apr_status_t md_http_perform_cb(md_http_request_t *req); ++ ++typedef 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; ++}; ++ ++void md_http_use_implementation(md_http_impl_t *impl); ++ ++ ++ ++#endif /* md_http_h */ diff --cc modules/md/md_json.c index 0000000000,0000000000..fc7b13f8de new file mode 100644 --- /dev/null +++ b/modules/md/md_json.c @@@ -1,0 -1,0 +1,1016 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#include "md_json.h" ++#include "md_log.h" ++#include "md_http.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 ++ */ ++#pragma GCC diagnostic push ++#pragma GCC diagnostic ignored "-Wunknown-pragmas" ++#pragma GCC diagnostic ignored "-Wunreachable-code" ++#pragma clang diagnostic push ++#pragma clang diagnostic ignored "-Wunused-function" ++ ++#include ++#undef JSON_INLINE ++#define JSON_INLINE ++#include ++ ++#pragma clang diagnostic pop ++#pragma GCC diagnostic pop ++ ++struct md_json_t { ++ apr_pool_t *p; ++ json_t *j; ++}; ++ ++/**************************************************************************************************/ ++/* lifecylce */ ++ ++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) { ++ json_decref(json->j); ++ json->j = NULL; ++ } ++} ++ ++md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json) ++{ ++ return json_create(pool, json_copy(json->j)); ++} ++ ++md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json) ++{ ++ return json_create(pool, json_deep_copy(json->j)); ++} ++ ++/**************************************************************************************************/ ++/* selectors */ ++ ++ ++static json_t *jselect(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)) { ++ 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; ++ } ++ ++ json_array_append(aj, 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) { ++ json_decref(val); ++ return APR_EINVAL; ++ } ++ ++ if (key) { ++ if (!json_is_object(j)) { ++ json_decref(val); ++ return APR_EINVAL; ++ } ++ json_object_set(j, key, val); ++ } ++ 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(md_json_t *json, ...) ++{ ++ json_t *j; ++ va_list ap; ++ ++ va_start(ap, json); ++ j = jselect(json, ap); ++ va_end(ap); ++ ++ return j != NULL; ++} ++ ++/**************************************************************************************************/ ++/* booleans */ ++ ++int md_json_getb(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(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(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(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, 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; ++} ++ ++/**************************************************************************************************/ ++/* 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; ++} ++ ++apr_status_t md_json_setj(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(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; ++} ++ ++ ++/**************************************************************************************************/ ++/* 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, 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, 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, ++ 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; ++} ++ ++/**************************************************************************************************/ ++/* array strings */ ++ ++apr_status_t md_json_getsa(apr_array_header_t *a, 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; ++ ++ 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 { ++ md_json_t *json; ++ md_json_fmt_t fmt; ++ const char *fname; ++ apr_file_t *f; ++} j_write_ctx; ++ ++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(md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb) ++{ ++ size_t flags = (fmt == MD_JSON_FMT_COMPACT)? JSON_COMPACT : JSON_INDENT(2); ++ int rv = json_dump_callback(json->j, dump_cb, bb, flags); ++ 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 = apr_pcalloc(chunks->pool, len+1); ++ ++ memcpy(chunk, buffer, len); ++ APR_ARRAY_PUSH(chunks, const char *) = chunk; ++ return 0; ++} ++ ++const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) ++{ ++ size_t flags = (fmt == MD_JSON_FMT_COMPACT)? JSON_COMPACT : JSON_INDENT(2); ++ apr_array_header_t *chunks; ++ int rv; ++ ++ chunks = apr_array_make(p, 10, sizeof(char *)); ++ rv = json_dump_callback(json->j, chunk_cb, chunks, flags); ++ ++ if (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(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f) ++{ ++ apr_status_t rv; ++ const char *s; ++ ++ s = md_json_writep(json, p, fmt); ++ ++ if (s) { ++ rv = apr_file_write_full(f, s, strlen(s), NULL); ++ } ++ else { ++ rv = APR_EINVAL; ++ } ++ ++ if (APR_SUCCESS != rv) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef"); ++ } ++ return rv; ++} ++ ++apr_status_t md_json_fcreatex(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(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) { ++ return APR_EINVAL; ++ } ++ *pjson = json_create(pool, j); ++ return APR_SUCCESS; ++} ++ ++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; ++ if (res->rv == APR_SUCCESS) { ++ const char *ctype = apr_table_get(res->headers, "content-type"); ++ if (ctype && res->body && (strstr(ctype, "/json") || strstr(ctype, "+json"))) { ++ rv = md_json_readb(pjson, pool, res->body); ++ } ++ } ++ 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) ++{ ++ resp_data *resp = res->req->baton; ++ 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) ++{ ++ long req_id; ++ apr_status_t rv; ++ resp_data resp; ++ ++ memset(&resp, 0, sizeof(resp)); ++ resp.pool = pool; ++ ++ rv = md_http_GET(http, url, NULL, json_resp_cb, &resp, &req_id); ++ ++ if (rv == APR_SUCCESS) { ++ md_http_await(http, req_id); ++ *pjson = resp.json; ++ return resp.rv; ++ } ++ *pjson = NULL; ++ return rv; ++} ++ diff --cc modules/md/md_json.h index 0000000000,0000000000..dde3d240e3 new file mode 100644 --- /dev/null +++ b/modules/md/md_json.h @@@ -1,0 -1,0 +1,121 @@@ ++/* Copyright 2017 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 mod_md_md_json_h ++#define mod_md_md_json_h ++ ++#include ++ ++struct apr_bucket_brigade; ++struct apr_file_t; ++ ++struct md_http_t; ++struct md_http_response_t; ++ ++ ++typedef struct md_json_t md_json_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, md_json_t *json); ++md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json); ++ ++int md_json_has_key(md_json_t *json, ...); ++ ++/* boolean manipulation */ ++int md_json_getb(md_json_t *json, ...); ++apr_status_t md_json_setb(int value, md_json_t *json, ...); ++ ++/* number manipulation */ ++double md_json_getn(md_json_t *json, ...); ++apr_status_t md_json_setn(double value, md_json_t *json, ...); ++ ++/* long manipulation */ ++long md_json_getl(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(md_json_t *json, ...); ++const char *md_json_dups(apr_pool_t *p, md_json_t *json, ...); ++apr_status_t md_json_sets(const char *s, md_json_t *json, ...); ++ ++/* json manipulation */ ++md_json_t *md_json_getj(md_json_t *json, ...); ++apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...); ++apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...); ++ ++/* Array/Object manipulation */ ++apr_status_t md_json_clr(md_json_t *json, ...); ++apr_status_t md_json_del(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, 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, 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, ...); ++ ++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, ...); ++ ++/* Manipulating Object String values */ ++apr_status_t md_json_gets_dict(apr_table_t *dict, 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, 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(md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); ++const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); ++apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, ++ md_json_fmt_t fmt, struct apr_file_t *f); ++apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, ++ const char *fpath, apr_fileperms_t perms); ++apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, ++ 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); ++ ++#endif /* md_json_h */ diff --cc modules/md/md_jws.c index 0000000000,0000000000..f3788b3f88 new file mode 100644 --- /dev/null +++ b/modules/md/md_jws.c @@@ -1,0 -1,0 +1,105 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#include "md_crypt.h" ++#include "md_json.h" ++#include "md_jws.h" ++#include "md_log.h" ++#include "md_util.h" ++ ++static int header_set(void *data, const char *key, const char *val) ++{ ++ md_json_sets(val, (md_json_t *)data, key, NULL); ++ return 1; ++} ++ ++apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, ++ const char *payload, size_t len, ++ struct apr_table_t *protected, ++ struct md_pkey_t *pkey, const char *key_id) ++{ ++ md_json_t *msg, *jprotected; ++ const char *prot64, *pay64, *sign64, *sign, *prot; ++ apr_status_t rv = APR_SUCCESS; ++ ++ *pmsg = NULL; ++ ++ msg = md_json_create(p); ++ ++ jprotected = md_json_create(p); ++ md_json_sets("RS256", jprotected, "alg", NULL); ++ if (key_id) { ++ md_json_sets(key_id, jprotected, "kid", NULL); ++ } ++ else { ++ md_json_sets(md_pkey_get_rsa_e64(pkey, p), jprotected, "jwk", "e", NULL); ++ md_json_sets("RSA", jprotected, "jwk", "kty", NULL); ++ md_json_sets(md_pkey_get_rsa_n64(pkey, p), jprotected, "jwk", "n", NULL); ++ } ++ apr_table_do(header_set, jprotected, protected, NULL); ++ prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, p, "protected: %s", ++ prot ? prot : ""); ++ ++ if (!prot) { ++ rv = APR_EINVAL; ++ } ++ ++ if (rv == APR_SUCCESS) { ++ prot64 = md_util_base64url_encode(prot, strlen(prot), p); ++ md_json_sets(prot64, msg, "protected", NULL); ++ pay64 = md_util_base64url_encode(payload, len, 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 (rv == APR_SUCCESS) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, ++ "jws pay64=%s\nprot64=%s\nsign64=%s", pay64, prot64, sign64); ++ ++ md_json_sets(sign64, msg, "signature", NULL); ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message"); ++ } ++ ++ *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; ++ 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); ++ rv = md_crypt_sha256_digest64(pthumb, p, s, strlen(s)); ++ return rv; ++} diff --cc modules/md/md_jws.h index 0000000000,0000000000..bd3a006893 new file mode 100644 --- /dev/null +++ b/modules/md/md_jws.h @@@ -1,0 -1,0 +1,29 @@@ ++/* Copyright 2017 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 mod_md_md_jws_h ++#define mod_md_md_jws_h ++ ++struct apr_table_t; ++struct md_json_t; ++struct md_pkey_t; ++ ++apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, ++ const char *payload, size_t len, struct apr_table_t *protected, ++ struct md_pkey_t *pkey, const char *key_id); ++ ++apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey); ++ ++#endif /* md_jws_h */ diff --cc modules/md/md_log.c index 0000000000,0000000000..de2bcbdca5 new file mode 100644 --- /dev/null +++ b/modules/md/md_log.c @@@ -1,0 -1,0 +1,80 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++ ++#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) ++{ ++ if ((int)level < (sizeof(level_names)/sizeof(level_names[0]))) { ++ return level_names[level]; ++ } ++ return "???"; ++} ++ ++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 --cc modules/md/md_log.h index 0000000000,0000000000..9af15a723c new file mode 100644 --- /dev/null +++ b/modules/md/md_log.h @@@ -1,0 -1,0 +1,55 @@@ ++/* Copyright 2017 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 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__ ++ ++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, ...); ++ ++ ++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 --cc modules/md/md_os.c index 0000000000,0000000000..3379acbc2a new file mode 100644 --- /dev/null +++ b/modules/md/md_os.c @@@ -1,0 -1,0 +1,82 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#if APR_HAVE_UNISTD_H ++#include ++#endif ++#ifdef WIN32 ++#include "mpm_winnt.h" ++#endif ++#if AP_NEED_SET_MUTEX_PERMS ++#include "unixd.h" ++#endif ++ ++#include "md_util.h" ++#include "md_os.h" ++ ++apr_status_t md_try_chown(const char *fname, int uid, int gid, apr_pool_t *p) ++{ ++#if AP_NEED_SET_MUTEX_PERMS ++ 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() ++ "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) ++{ ++#if AP_NEED_SET_MUTEX_PERMS ++ return md_try_chown(fname, ap_unixd_config.user_id, -1, p); ++#else ++ return APR_ENOTIMPL; ++#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; ++ ++ 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 --cc modules/md/md_os.h index 0000000000,0000000000..9f5c2b6e8f new file mode 100644 --- /dev/null +++ b/modules/md/md_os.h @@@ -1,0 -1,0 +1,36 @@@ ++/* Copyright 2017 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 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, 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 --cc modules/md/md_reg.c index 0000000000,0000000000..b2953fb909 new file mode 100644 --- /dev/null +++ b/modules/md/md_reg.c @@@ -1,0 -1,0 +1,963 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include "md.h" ++#include "md_crypt.h" ++#include "md_log.h" ++#include "md_json.h" ++#include "md_reg.h" ++#include "md_store.h" ++#include "md_util.h" ++ ++#include "md_acme.h" ++#include "md_acme_acct.h" ++ ++struct md_reg_t { ++ struct md_store_t *store; ++ struct apr_hash_t *protos; ++ int was_synched; ++ int can_http; ++ int can_https; ++}; ++ ++/**************************************************************************************************/ ++/* life cycle */ ++ ++apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *p, struct md_store_t *store) ++{ ++ md_reg_t *reg; ++ apr_status_t rv; ++ ++ reg = apr_pcalloc(p, sizeof(*reg)); ++ reg->store = store; ++ reg->protos = apr_hash_make(p); ++ reg->can_http = 1; ++ reg->can_https = 1; ++ ++ rv = md_acme_protos_add(reg->protos, 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_util_is_dns_name(p, domain, 1)) { ++ 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_url) { /* setting to empty is ok */ ++ rv = md_util_abs_uri_check(p, md->ca_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, md->ca_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) { /* 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_UNKNOWN; ++ const md_creds_t *creds; ++ const md_cert_t *cert; ++ apr_time_t expires = 0; ++ apr_status_t rv; ++ int i; ++ ++ if (APR_SUCCESS == (rv = md_reg_creds_get(&creds, reg, MD_SG_DOMAINS, md, p))) { ++ state = MD_S_INCOMPLETE; ++ if (!creds->pkey) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "md{%s}: incomplete, without private key", md->name); ++ } ++ else if (!creds->cert) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "md{%s}: incomplete, has key but no certificate", md->name); ++ } ++ else if (!creds->chain) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "md{%s}: incomplete, has key and certificate, but no chain file.", ++ md->name); ++ } ++ else { ++ expires = md_cert_get_not_after(creds->cert); ++ if (md_cert_has_expired(creds->cert)) { ++ state = MD_S_EXPIRED; ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "md{%s}: expired, certificate has expired", md->name); ++ goto out; ++ } ++ if (!md_cert_is_valid_now(creds->cert)) { ++ state = MD_S_ERROR; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, ++ "md{%s}: error, certificate valid in future (clock wrong?)", ++ md->name); ++ goto out; ++ } ++ if (!md_cert_covers_md(creds->cert, md)) { ++ state = MD_S_INCOMPLETE; ++ md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, ++ "md{%s}: incomplete, cert no longer covers all domains, " ++ "needs sign up for a new certificate", md->name); ++ goto out; ++ } ++ ++ for (i = 0; i < creds->chain->nelts; ++i) { ++ cert = APR_ARRAY_IDX(creds->chain, i, const md_cert_t *); ++ if (!md_cert_is_valid_now(cert)) { ++ state = MD_S_ERROR; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, ++ "md{%s}: error, the certificate itself is valid, however the %d. " ++ "certificate in the chain is not valid now (clock wrong?).", ++ md->name, i); ++ goto out; ++ } ++ } ++ ++ state = MD_S_COMPLETE; ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: is complete", md->name); ++ } ++ } ++ ++out: ++ if (APR_SUCCESS != rv) { ++ state = MD_S_ERROR; ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name); ++ } ++ md->state = state; ++ md->expires = expires; ++ return rv; ++} ++ ++static apr_status_t state_vinit(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) ++{ ++ md_reg_t *reg = baton; ++ md_t *md = va_arg(ap, md_t *); ++ ++ return state_init(reg, p, md); ++} ++ ++static apr_status_t md_state_init(md_reg_t *reg, md_t *md, apr_pool_t *p) ++{ ++ return md_util_pool_vdo(state_vinit, reg, p, md, NULL); ++} ++ ++static md_t *state_check(md_reg_t *reg, md_t *md, apr_pool_t *p) ++{ ++ if (md) { ++ int ostate = md->state; ++ if (APR_SUCCESS == state_init(reg, p, md) && md->state != ostate) { ++ md_save(reg->store, p, MD_SG_DOMAINS, md, 0); ++ } ++ } ++ return md; ++} ++ ++apr_status_t md_reg_assess(md_reg_t *reg, md_t *md, int *perrored, int *prenew, apr_pool_t *p) ++{ ++ int renew = 0; ++ int errored = 0; ++ apr_time_t now = apr_time_now(); ++ ++ switch (md->state) { ++ case MD_S_UNKNOWN: ++ md_log_perror( MD_LOG_MARK, MD_LOG_ERR, 0, p, "md(%s): in unkown state.", md->name); ++ break; ++ case MD_S_ERROR: ++ md_log_perror( MD_LOG_MARK, MD_LOG_ERR, 0, p, ++ "md(%s): in error state, unable to drive forward. It could " ++ "be that files have gotten corrupted. You may check with " ++ "a2md the status of this managed domain to diagnose the " ++ " problem. As a last resort, you may delete the files for " ++ " this md and start all over.", md->name); ++ errored = 1; ++ break; ++ case MD_S_COMPLETE: ++ if (!md->expires) { ++ md_log_perror( MD_LOG_MARK, MD_LOG_WARNING, 0, p, ++ "md(%s): looks complete, but has unknown expiration date.", md->name); ++ errored = 1; ++ } ++ else if (md->expires <= now) { ++ /* Maybe we hibernated in the meantime? */ ++ md->state = MD_S_EXPIRED; ++ renew = 1; ++ } ++ else if ((md->expires - now) <= md->renew_window) { ++ int days = (int)(apr_time_sec(md->expires - now) / MD_SECS_PER_DAY); ++ md_log_perror( MD_LOG_MARK, MD_LOG_DEBUG, 0, p, ++ "md(%s): %d days to expiry, attempt renewal", md->name, days); ++ renew = 1; ++ } ++ break; ++ case MD_S_INCOMPLETE: ++ case MD_S_EXPIRED: ++ renew = 1; ++ break; ++ } ++ *prenew = renew; ++ *perrored = errored; ++ return APR_SUCCESS; ++} ++ ++/**************************************************************************************************/ ++/* 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; ++ ++ if (!ctx->exclude || strcmp(ctx->exclude, md->name)) { ++ md = state_check(ctx->reg, (md_t*)md, ptemp); ++ 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)) { ++ return state_check(reg, md, p); ++ } ++ 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; ++ ++ if (md_contains(md, ctx->domain)) { ++ 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); ++ return state_check(reg, (md_t*)ctx.md, p); ++} ++ ++typedef struct { ++ const md_t *md_checked; ++ const 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; ++ ++ 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; ++ } ++ return state_check(reg, (md_t*)ctx.md, p); ++} ++ ++apr_status_t md_reg_get_cred_files(md_reg_t *reg, const md_t *md, apr_pool_t *p, ++ const char **pkeyfile, const char **pcertfile, ++ const char **pchainfile) ++{ ++ apr_status_t rv; ++ ++ rv = md_store_get_fname(pkeyfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_PKEY, p); ++ if (APR_SUCCESS == rv) { ++ rv = md_store_get_fname(pcertfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_CERT, p); ++ } ++ if (APR_SUCCESS == rv) { ++ rv = md_store_get_fname(pchainfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_CHAIN, p); ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ *pchainfile = NULL; ++ rv = APR_SUCCESS; ++ } ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* 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; ++ ++ md = va_arg(ap, md_t *); ++ mine = md_clone(ptemp, md); ++ if (APR_SUCCESS == (rv = check_values(reg, ptemp, md, MD_UPD_ALL)) ++ && APR_SUCCESS == (rv = md_state_init(reg, mine, ptemp)) ++ && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, mine, 1))) { ++ } ++ return rv; ++} ++ ++apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p) ++{ ++ return md_util_pool_vdo(p_md_add, reg, p, md, NULL); ++} ++ ++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; ++ md_t *nmd; ++ ++ name = va_arg(ap, const char *); ++ updates = va_arg(ap, const md_t *); ++ fields = 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, "update md %s", name); ++ ++ if (APR_SUCCESS != (rv = check_values(reg, ptemp, updates, fields))) { ++ return rv; ++ } ++ ++ 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_url = updates->ca_url; ++ 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_CERT_URL & fields) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update cert url: %s", name); ++ nmd->cert_url = updates->cert_url; ++ } ++ if (MD_UPD_DRIVE_MODE & fields) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update drive-mode: %s", name); ++ nmd->drive_mode = updates->drive_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_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 (fields && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, nmd, 0))) { ++ rv = md_state_init(reg, nmd, ptemp); ++ } ++ 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) ++{ ++ return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, NULL); ++} ++ ++/**************************************************************************************************/ ++/* certificate related */ ++ ++static int ok_or_noent(apr_status_t rv) ++{ ++ return (APR_SUCCESS == rv || APR_ENOENT == rv); ++} ++ ++static apr_status_t creds_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) ++{ ++ md_reg_t *reg = baton; ++ apr_status_t rv; ++ md_cert_t *cert; ++ md_pkey_t *pkey; ++ apr_array_header_t *chain; ++ md_creds_t *creds, **pcreds; ++ const md_t *md; ++ md_cert_state_t cert_state; ++ md_store_group_t group; ++ ++ pcreds = va_arg(ap, md_creds_t **); ++ group = va_arg(ap, int); ++ md = va_arg(ap, const md_t *); ++ ++ if (ok_or_noent(rv = md_cert_load(reg->store, group, md->name, &cert, p)) ++ && ok_or_noent(rv = md_pkey_load(reg->store, group, md->name, &pkey, p)) ++ && ok_or_noent(rv = md_chain_load(reg->store, group, md->name, &chain, p))) { ++ rv = APR_SUCCESS; ++ ++ creds = apr_pcalloc(p, sizeof(*creds)); ++ creds->cert = cert; ++ creds->pkey = pkey; ++ creds->chain = chain; ++ ++ if (creds->cert) { ++ switch ((cert_state = md_cert_state_get(creds->cert))) { ++ case MD_CERT_VALID: ++ creds->expired = 0; ++ break; ++ case MD_CERT_EXPIRED: ++ creds->expired = 1; ++ 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; ++ } ++ } ++ } ++ *pcreds = (APR_SUCCESS == rv)? creds : NULL; ++ return rv; ++} ++ ++apr_status_t md_reg_creds_get(const md_creds_t **pcreds, md_reg_t *reg, ++ md_store_group_t group, const md_t *md, apr_pool_t *p) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ md_creds_t *creds; ++ ++ rv = md_util_pool_vdo(creds_load, reg, p, &creds, group, md, NULL); ++ *pcreds = (APR_SUCCESS == rv)? creds : NULL; ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* synching */ ++ ++typedef struct { ++ apr_pool_t *p; ++ apr_array_header_t *conf_mds; ++ apr_array_header_t *store_mds; ++} sync_ctx; ++ ++static int find_changes(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp) ++{ ++ sync_ctx *ctx = baton; ++ ++ APR_ARRAY_PUSH(ctx->store_mds, const md_t*) = md_clone(ctx->p, md); ++ return 1; ++} ++ ++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; ++} ++ ++static apr_status_t sync_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https) ++{ ++ md_json_t *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); ++} ++ ++/** ++ * Procedure: ++ * 1. Collect all defined "managed domains" (MD). It does not matter where a MD is defined. ++ * All MDs need to be unique and have no overlaps in their domain names. ++ * Fail the config otherwise. Also, if a vhost matches an MD, it ++ * needs to *only* have ServerAliases from that MD. There can be no more than one ++ * matching MD for a vhost. But an MD can apply to several vhosts. ++ * 2. Synchronize with the persistent store. Iterate over all configured MDs and ++ * a. create them in the store if they do not already exist, neither under the ++ * name or with a common domain. ++ * b. compare domain lists from store and config, if ++ * - store has dns name in other MD than from config, remove dns name from store def, ++ * issue WARNING. ++ * - store misses dns name from config, add dns name and update store ++ * c. compare MD acme url/protocol, update if changed ++ */ ++apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, ++ apr_array_header_t *master_mds, int can_http, int can_https) ++{ ++ sync_ctx ctx; ++ md_store_t *store = reg->store; ++ apr_status_t rv; ++ ++ if (APR_SUCCESS != (rv = sync_props(reg, ptemp, can_http, can_https))) { ++ reg->was_synched = 0; ++ return rv; ++ } ++ ++ reg->was_synched = 1; ++ ++ ctx.p = ptemp; ++ ctx.conf_mds = master_mds; ++ ctx.store_mds = apr_array_make(ptemp, 100, sizeof(md_t *)); ++ ++ rv = md_store_md_iter(find_changes, &ctx, store, ptemp, MD_SG_DOMAINS, "*"); ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ rv = APR_SUCCESS; ++ } ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "sync: found %d mds in store", ctx.store_mds->nelts); ++ if (APR_SUCCESS == rv) { ++ int i, added, fields; ++ md_t *md, *config_md, *smd, *omd; ++ const char *common; ++ ++ for (i = 0; i < ctx.conf_mds->nelts; ++i) { ++ md = APR_ARRAY_IDX(ctx.conf_mds, i, md_t *); ++ ++ /* find the store md that is closest match for the configured md */ ++ smd = md_find_closest_match(ctx.store_mds, md); ++ if (smd) { ++ fields = 0; ++ /* add any newly configured domains to the store md */ ++ added = md_array_str_add_missing(smd->domains, md->domains, 0); ++ if (added) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "%s: %d domains added", smd->name, added); ++ fields |= MD_UPD_DOMAINS; ++ } ++ ++ /* Look for other store mds which have domains now being part of smd */ ++ while (APR_SUCCESS == rv && (omd = md_get_by_dns_overlap(ctx.store_mds, md))) { ++ /* find the name now duplicate */ ++ common = md_common_name(md, omd); ++ assert(common); ++ ++ /* Is this md still configured or has it been abandoned in the config? */ ++ config_md = md_get_by_name(ctx.conf_mds, omd->name); ++ if (config_md && md_contains(config_md, common)) { ++ /* domain used in two configured mds, not allowed */ ++ rv = APR_EINVAL; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, ++ "domain %s used in md %s and %s", ++ common, md->name, omd->name); ++ } ++ else if (config_md) { ++ /* domain stored in omd, but no longer has the offending domain, ++ remove it from the store md. */ ++ omd->domains = md_array_str_remove(ptemp, omd->domains, common, 0); ++ rv = md_reg_update(reg, ptemp, omd->name, omd, MD_UPD_DOMAINS); ++ } ++ else { ++ /* domain in a store md that is no longer configured, warn about it. ++ * Remove the domain here, so we can progress, but never save it. */ ++ omd->domains = md_array_str_remove(ptemp, omd->domains, common, 0); ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, ++ "domain %s, configured in md %s, is part of the stored md %s." ++ " That md however is no longer mentioned in the config. " ++ "If you longer want it, remove the md from the store.", ++ common, md->name, omd->name); ++ } ++ } ++ ++ if (MD_SVAL_UPDATE(md, smd, ca_url)) { ++ smd->ca_url = md->ca_url; ++ fields |= MD_UPD_CA_URL; ++ } ++ if (MD_SVAL_UPDATE(md, smd, ca_proto)) { ++ smd->ca_proto = md->ca_proto; ++ fields |= MD_UPD_CA_PROTO; ++ } ++ if (MD_SVAL_UPDATE(md, smd, ca_agreement)) { ++ smd->ca_agreement = md->ca_agreement; ++ fields |= MD_UPD_AGREEMENT; ++ } ++ if (MD_VAL_UPDATE(md, smd, drive_mode)) { ++ smd->drive_mode = md->drive_mode; ++ fields |= MD_UPD_DRIVE_MODE; ++ } ++ if (!apr_is_empty_array(md->contacts) ++ && !md_array_str_eq(md->contacts, smd->contacts, 0)) { ++ smd->contacts = md->contacts; ++ fields |= MD_UPD_CONTACTS; ++ } ++ if (MD_VAL_UPDATE(md, smd, renew_window)) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, ++ "%s: update renew_window, old=%ld, new=%ld", ++ smd->name, (long)smd->renew_window, md->renew_window); ++ smd->renew_window = md->renew_window; ++ fields |= MD_UPD_RENEW_WINDOW; ++ } ++ if (md->ca_challenges) { ++ md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0); ++ if (smd->ca_challenges ++ && !md_array_str_eq(md->ca_challenges, smd->ca_challenges, 0)) { ++ smd->ca_challenges = (md->ca_challenges? ++ apr_array_copy(ptemp, md->ca_challenges) : NULL); ++ fields |= MD_UPD_CA_CHALLENGES; ++ } ++ } ++ ++ if (fields) { ++ rv = md_reg_update(reg, ptemp, smd->name, smd, fields); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md %s updated", smd->name); ++ } ++ } ++ else { ++ /* new managed domain */ ++ rv = md_reg_add(reg, md, ptemp); ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "new md %s added", md->name); ++ } ++ } ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading mds"); ++ } ++ ++ return rv; ++} ++ ++ ++/**************************************************************************************************/ ++/* driving */ ++ ++static apr_status_t init_proto_driver(md_proto_driver_t *driver, const md_proto_t *proto, ++ md_reg_t *reg, const md_t *md, ++ const char *challenge, int reset, apr_pool_t *p) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ ++ /* If this registry instance was not synched before (and obtained server ++ * properties that way), read them from the store. ++ */ ++ if (reg->was_synched ++ || APR_SUCCESS == (rv = load_props(reg, p))) { ++ ++ driver->proto = proto; ++ driver->p = p; ++ driver->challenge = challenge; ++ driver->can_http = reg->can_http; ++ driver->can_https = reg->can_https; ++ driver->reg = reg; ++ driver->store = md_reg_store_get(reg); ++ driver->md = md; ++ driver->reset = reset; ++ } ++ ++ return rv; ++} ++ ++static apr_status_t run_stage(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) ++{ ++ md_reg_t *reg = baton; ++ const md_proto_t *proto; ++ const md_t *md; ++ int reset; ++ md_proto_driver_t *driver; ++ const char *challenge; ++ apr_status_t rv; ++ ++ proto = va_arg(ap, const md_proto_t *); ++ md = va_arg(ap, const md_t *); ++ challenge = va_arg(ap, const char *); ++ reset = va_arg(ap, int); ++ ++ driver = apr_pcalloc(ptemp, sizeof(*driver)); ++ rv = init_proto_driver(driver, proto, reg, md, challenge, reset, ptemp); ++ if (APR_SUCCESS == rv && ++ APR_SUCCESS == (rv = proto->init(driver))) { ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run staging", md->name); ++ rv = proto->stage(driver); ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: staging done", md->name); ++ return rv; ++} ++ ++apr_status_t md_reg_stage(md_reg_t *reg, const md_t *md, const char *challenge, ++ int reset, apr_pool_t *p) ++{ ++ const md_proto_t *proto; ++ ++ if (!md->ca_proto) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md %s has no CA protocol", md->name); ++ ((md_t *)md)->state = MD_S_ERROR; ++ return APR_SUCCESS; ++ } ++ ++ proto = apr_hash_get(reg->protos, md->ca_proto, strlen(md->ca_proto)); ++ if (!proto) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, ++ "md %s has unknown CA protocol: %s", md->name, md->ca_proto); ++ ((md_t *)md)->state = MD_S_ERROR; ++ return APR_EINVAL; ++ } ++ ++ return md_util_pool_vdo(run_stage, reg, p, proto, md, challenge, reset, NULL); ++} ++ ++static apr_status_t run_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) ++{ ++ md_reg_t *reg = baton; ++ const char *name; ++ const md_proto_t *proto; ++ const md_t *md, *nmd; ++ md_proto_driver_t *driver; ++ apr_status_t rv; ++ ++ name = va_arg(ap, const char *); ++ ++ if (APR_STATUS_IS_ENOENT(rv = md_load(reg->store, MD_SG_STAGING, name, NULL, ptemp))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "%s: nothing staged", name); ++ return APR_ENOENT; ++ } ++ ++ md = md_reg_get(reg, name, p); ++ if (!md) { ++ return APR_ENOENT; ++ } ++ ++ if (!md->ca_proto) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md %s has no CA protocol", name); ++ ((md_t *)md)->state = MD_S_ERROR; ++ return APR_EINVAL; ++ } ++ ++ proto = apr_hash_get(reg->protos, md->ca_proto, strlen(md->ca_proto)); ++ if (!proto) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, ++ "md %s has unknown CA protocol: %s", md->name, md->ca_proto); ++ ((md_t *)md)->state = MD_S_ERROR; ++ return APR_EINVAL; ++ } ++ ++ driver = apr_pcalloc(ptemp, sizeof(*driver)); ++ init_proto_driver(driver, proto, reg, md, NULL, 0, ptemp); ++ ++ if (APR_SUCCESS == (rv = proto->init(driver))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run load", md->name); ++ ++ if (APR_SUCCESS == (rv = proto->preload(driver, MD_SG_TMP))) { ++ /* swap */ ++ rv = md_store_move(reg->store, p, MD_SG_TMP, MD_SG_DOMAINS, md->name, 1); ++ if (APR_SUCCESS == rv) { ++ /* load again */ ++ nmd = md_reg_get(reg, md->name, p); ++ if (!nmd) { ++ rv = APR_ENOENT; ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading md after staging"); ++ } ++ else if (nmd->state != MD_S_COMPLETE) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, ++ "md has state %d after load", nmd->state); ++ } ++ ++ md_store_purge(reg->store, p, MD_SG_STAGING, md->name); ++ md_store_purge(reg->store, p, MD_SG_CHALLENGES, md->name); ++ } ++ } ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: load done", md->name); ++ return rv; ++} ++ ++apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p) ++{ ++ return md_util_pool_vdo(run_load, reg, p, name, NULL); ++} ++ ++apr_status_t md_reg_drive(md_reg_t *reg, md_t *md, const char *challenge, ++ int reset, int force, apr_pool_t *p) ++{ ++ apr_status_t rv; ++ int errored, renew; ++ ++ if (APR_SUCCESS == (rv = md_reg_assess(reg, md, &errored, &renew, p))) { ++ if (errored) { ++ rv = APR_EGENERAL; ++ } ++ else if (renew || force) { ++ if (APR_SUCCESS == (rv = md_reg_stage(reg, md, challenge, reset, p))) { ++ rv = md_reg_load(reg, md->name, p); ++ } ++ } ++ } ++ return rv; ++} diff --cc modules/md/md_reg.h index 0000000000,0000000000..d3af921903 new file mode 100644 --- /dev/null +++ b/modules/md/md_reg.h @@@ -1,0 -1,0 +1,182 @@@ ++/* Copyright 2017 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 mod_md_md_reg_h ++#define mod_md_md_reg_h ++ ++struct apr_hash_t; ++struct apr_array_header_t; ++struct md_store_t; ++struct md_pkey_t; ++struct md_cert_t; ++ ++/** ++ * A registry for managed domains with a md_store_t as persistence. ++ * ++ */ ++typedef struct md_reg_t md_reg_t; ++ ++/** ++ * Initialize the registry, using the pool and loading any existing information ++ * from the store. ++ */ ++apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *pm, struct md_store_t *store); ++ ++struct md_store_t *md_reg_store_get(md_reg_t *reg); ++ ++/** ++ * 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); ++ ++/** ++ * Assess the capability and need to driving this managed domain. ++ */ ++apr_status_t md_reg_assess(md_reg_t *reg, md_t *md, int *perrored, int *prenew, 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 guarantueed. ++ * 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 0x0001 ++#define MD_UPD_CA_URL 0x0002 ++#define MD_UPD_CA_PROTO 0x0004 ++#define MD_UPD_CA_ACCOUNT 0x0008 ++#define MD_UPD_CONTACTS 0x0010 ++#define MD_UPD_AGREEMENT 0x0020 ++#define MD_UPD_CERT_URL 0x0040 ++#define MD_UPD_DRIVE_MODE 0x0080 ++#define MD_UPD_RENEW_WINDOW 0x0100 ++#define MD_UPD_CA_CHALLENGES 0x0200 ++#define MD_UPD_ALL 0x7FFF ++ ++/** ++ * 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); ++ ++/** ++ * Get the credentials available for the managed domain md. Returns APR_ENOENT ++ * when none is available. The returned values are immutable. ++ */ ++apr_status_t md_reg_creds_get(const md_creds_t **pcreds, md_reg_t *reg, ++ md_store_group_t group, const md_t *md, apr_pool_t *p); ++ ++apr_status_t md_reg_get_cred_files(md_reg_t *reg, const md_t *md, apr_pool_t *p, ++ const char **pkeyfile, const char **pcertfile, ++ const char **pchainfile); ++ ++/** ++ * Synchronise the give master mds with the store. ++ */ ++apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, ++ apr_array_header_t *master_mds, int can_http, int can_https); ++ ++/**************************************************************************************************/ ++/* protocol drivers */ ++ ++typedef struct md_proto_t md_proto_t; ++ ++typedef struct md_proto_driver_t md_proto_driver_t; ++ ++struct md_proto_driver_t { ++ const md_proto_t *proto; ++ apr_pool_t *p; ++ const char *challenge; ++ int can_http; ++ int can_https; ++ struct md_store_t *store; ++ md_reg_t *reg; ++ const md_t *md; ++ void *baton; ++ int reset; ++}; ++ ++typedef apr_status_t md_proto_init_cb(md_proto_driver_t *driver); ++typedef apr_status_t md_proto_stage_cb(md_proto_driver_t *driver); ++typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver, md_store_group_t group); ++ ++struct md_proto_t { ++ const char *protocol; ++ md_proto_init_cb *init; ++ md_proto_stage_cb *stage; ++ md_proto_preload_cb *preload; ++}; ++ ++ ++/** ++ * Stage a new credentials set for the given managed domain in a separate location ++ * without interfering with any existing credentials. ++ */ ++apr_status_t md_reg_stage(md_reg_t *reg, const md_t *md, ++ const char *challenge, int reset, apr_pool_t *p); ++ ++/** ++ * Load a staged set of new credentials for the managed domain. This will archive ++ * any existing credential data and make the staged set the new live one. ++ * If staging is incomplete or missing, the load will fail and all credentials remain ++ * as they are. ++ */ ++apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p); ++ ++/** ++ * Drive the given managed domain toward completeness. ++ * This is a convenience method that combines staging and, on success, loading ++ * of a new managed domain credentials set. ++ * ++ * @param reg the md registry ++ * @param md the managed domain to drive ++ * @param challenge the challenge type to use or NULL for auto selection ++ * @param reset remove any staging information that has been collected ++ * @param force force driving even though it looks unnecessary (e.g. not epxired) ++ * @param p pool to use ++ */ ++apr_status_t md_reg_drive(md_reg_t *reg, md_t *md, ++ const char *challenge, int reset, int force, apr_pool_t *p); ++ ++#endif /* mod_md_md_reg_h */ diff --cc modules/md/md_store.c index 0000000000,0000000000..9028845c6f new file mode 100644 --- /dev/null +++ b/modules/md/md_store.c @@@ -1,0 -1,0 +1,291 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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", ++ NULL ++}; ++ ++const char *md_store_group_name(int group) ++{ ++ if (group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) { ++ return GROUP_NAME[group]; ++ } ++ return "UNKNOWN"; ++} ++ ++void md_store_destroy(md_store_t *store) ++{ ++ if (store->destroy) store->destroy(store); ++} ++ ++apr_status_t md_store_load(md_store_t *store, md_store_group_t group, ++ const char *name, const char *aspect, ++ md_store_vtype_t vtype, void **pdata, ++ 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; ++} ++ ++/**************************************************************************************************/ ++/* 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; ++ ++ 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); ++} ++ ++typedef struct { ++ apr_pool_t *p; ++ apr_array_header_t *mds; ++} md_load_ctx; ++ ++apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, const char *name, ++ md_pkey_t **ppkey, apr_pool_t *p) ++{ ++ return md_store_load(store, group, name, MD_FN_PKEY, 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, ++ struct md_pkey_t *pkey, int create) ++{ ++ return md_store_save(store, p, group, name, MD_FN_PKEY, MD_SV_PKEY, pkey, create); ++} ++ ++apr_status_t md_cert_load(md_store_t *store, md_store_group_t group, const char *name, ++ struct md_cert_t **pcert, apr_pool_t *p) ++{ ++ return md_store_load(store, group, name, MD_FN_CERT, MD_SV_CERT, (void**)pcert, p); ++} ++ ++apr_status_t md_cert_save(md_store_t *store, apr_pool_t *p, ++ md_store_group_t group, const char *name, ++ struct md_cert_t *cert, int create) ++{ ++ return md_store_save(store, p, group, name, MD_FN_CERT, MD_SV_CERT, cert, create); ++} ++ ++apr_status_t md_chain_load(md_store_t *store, md_store_group_t group, const char *name, ++ struct apr_array_header_t **pchain, apr_pool_t *p) ++{ ++ return md_store_load(store, group, name, MD_FN_CHAIN, MD_SV_CHAIN, (void**)pchain, p); ++} ++ ++apr_status_t md_chain_save(md_store_t *store, apr_pool_t *p, ++ md_store_group_t group, const char *name, ++ struct apr_array_header_t *chain, int create) ++{ ++ return md_store_save(store, p, group, name, MD_FN_CHAIN, MD_SV_CHAIN, chain, create); ++} ++ ++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); ++} ++ diff --cc modules/md/md_store.h index 0000000000,0000000000..7806504bee new file mode 100644 --- /dev/null +++ b/modules/md/md_store.h @@@ -1,0 -1,0 +1,139 @@@ ++/* Copyright 2017 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 mod_md_md_store_h ++#define mod_md_md_store_h ++ ++struct apr_array_header_t; ++struct md_cert_t; ++struct md_pkey_t; ++ ++typedef struct md_store_t md_store_t; ++ ++typedef void md_store_destroy_cb(md_store_t *store); ++ ++const char *md_store_group_name(int group); ++ ++ ++typedef apr_status_t md_store_load_cb(md_store_t *store, md_store_group_t group, ++ const char *name, const char *aspect, ++ md_store_vtype_t vtype, void **pvalue, ++ apr_pool_t *p); ++typedef apr_status_t md_store_save_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, ++ const char *name, const char *aspect, ++ md_store_vtype_t vtype, void *value, ++ int create); ++typedef apr_status_t md_store_remove_cb(md_store_t *store, md_store_group_t group, ++ const char *name, const char *aspect, ++ apr_pool_t *p, int force); ++typedef apr_status_t md_store_purge_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, ++ const char *name); ++ ++typedef int md_store_inspect(void *baton, const char *name, const char *aspect, ++ md_store_vtype_t vtype, void *value, apr_pool_t *ptemp); ++ ++typedef apr_status_t md_store_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, ++ apr_pool_t *p, md_store_group_t group, const char *pattern, ++ const char *aspect, md_store_vtype_t vtype); ++ ++typedef apr_status_t md_store_move_cb(md_store_t *store, apr_pool_t *p, md_store_group_t from, ++ md_store_group_t to, const char *name, int archive); ++ ++typedef apr_status_t md_store_get_fname_cb(const char **pfname, ++ md_store_t *store, md_store_group_t group, ++ const char *name, const char *aspect, ++ apr_pool_t *p); ++ ++struct md_store_t { ++ md_store_destroy_cb *destroy; ++ ++ md_store_save_cb *save; ++ md_store_load_cb *load; ++ md_store_remove_cb *remove; ++ md_store_move_cb *move; ++ md_store_iter_cb *iterate; ++ md_store_purge_cb *purge; ++ md_store_get_fname_cb *get_fname; ++}; ++ ++void md_store_destroy(md_store_t *store); ++ ++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); ++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); ++ ++ ++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); ++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); ++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); ++apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, ++ md_store_group_t group, const char *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); ++ ++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); ++ ++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); ++ ++/**************************************************************************************************/ ++/* 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); ++ ++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); ++ ++ ++apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, ++ const char *name, 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_t *pkey, int create); ++apr_status_t md_cert_load(md_store_t *store, md_store_group_t group, ++ const char *name, struct md_cert_t **pcert, apr_pool_t *p); ++apr_status_t md_cert_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, ++ const char *name, struct md_cert_t *cert, int create); ++apr_status_t md_chain_load(md_store_t *store, md_store_group_t group, ++ const char *name, struct apr_array_header_t **pchain, apr_pool_t *p); ++apr_status_t md_chain_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, ++ const char *name, struct apr_array_header_t *chain, int create); ++ ++ ++#endif /* mod_md_md_store_h */ diff --cc modules/md/md_store_fs.c index 0000000000,0000000000..f456b8fc10 new file mode 100644 --- /dev/null +++ b/modules/md/md_store_fs.c @@@ -1,0 -1,0 +1,727 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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 */ ++ ++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; ++ ++ const unsigned char *key; ++ apr_size_t key_len; ++ int plain_pkey[MD_SG_COUNT]; ++ ++ int port_80; ++ int port_443; ++ ++ const unsigned char *dupkey; ++}; ++ ++#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_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_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_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 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; ++ unsigned char key[FS_STORE_KLEN]; ++ int i; ++ ++ md_json_sets(MOD_MD_VERSION, json, MD_KEY_VERSION, NULL); ++ ++ /*if (APR_SUCCESS != (rv = md_rand_bytes(key, sizeof(key), p))) { ++ return rv; ++ }*/ ++ for (i = 0; i < FS_STORE_KLEN; ++i) { ++ key[i] = 'a' + (i % 26); ++ } ++ ++ s_fs->key_len = sizeof(key); ++ s_fs->key = apr_pcalloc(p, sizeof(key) + 1); ++ memcpy((void*)s_fs->key, key, sizeof(key)); ++ s_fs->dupkey = apr_pmemdup(p, key, sizeof(key)); ++ ++ key64 = md_util_base64url_encode((char *)key, sizeof(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)); ++ ++ assert(memcmp(s_fs->key, s_fs->dupkey, FS_STORE_KLEN) == 0); ++ 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 *s, *key64; ++ apr_status_t rv; ++ ++ if (APR_SUCCESS == (rv = md_json_readf(&json, p, fname))) { ++ s = md_json_gets(json, MD_KEY_VERSION, NULL); ++ if (!s) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "missing key: %s", MD_KEY_VERSION); ++ return APR_EINVAL; ++ } ++ if (strcmp(MOD_MD_VERSION, s) < 0) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "version too new: %s", s); ++ return APR_EINVAL; ++ } ++ /* TODO: need to migrate store? */ ++ ++ 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; ++ } ++ ++ s_fs->key_len = md_util_base64url_decode((const char **)&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 too short: %d", s_fs->key_len); ++ return APR_EINVAL; ++ } ++ s_fs->dupkey = apr_pmemdup(p, s_fs->key, FS_STORE_KLEN); ++ } ++ 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; ++ ++ s_fs->plain_pkey[MD_SG_DOMAINS] = 1; ++ s_fs->plain_pkey[MD_SG_TMP] = 1; ++ ++ rv = md_util_path_merge(&fname, ptemp, s_fs->base, FS_STORE_JSON, NULL); ++ if (APR_SUCCESS != rv) { ++ return rv; ++ } ++ ++read: ++ if (APR_SUCCESS == (rv = md_util_is_file(fname, ptemp))) { ++ rv = read_store_file(s_fs, fname, p, ptemp); ++ } ++ else if (APR_STATUS_IS_ENOENT(rv)) { ++ rv = init_store_file(s_fs, fname, p, ptemp); ++ if (APR_STATUS_IS_EEXIST(rv)) { ++ 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.purge = fs_purge; ++ s_fs->s.iterate = fs_iterate; ++ s_fs->s.get_fname = fs_get_fname; ++ ++ /* 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; ++ ++ s_fs->base = apr_pstrdup(p, path); ++ ++ if (APR_SUCCESS != (rv = md_util_is_dir(s_fs->base, p))) { ++ if (APR_STATUS_IS_ENOENT(rv)) { ++ rv = apr_dir_make_recursive(s_fs->base, s_fs->def_perms.dir, p); ++ if (APR_SUCCESS == rv) { ++ rv = apr_file_perms_set(s_fs->base, MD_FPROT_D_UALL_WREAD); ++ if (APR_STATUS_IS_ENOTIMPL(rv)) { ++ rv = APR_SUCCESS; ++ } ++ } ++ } ++ } ++ 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", path); ++ } ++ *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 { ++ assert(memcmp(s_fs->key, s_fs->dupkey, FS_STORE_KLEN) == 0); ++ *ppass = (const char *)s_fs->key; ++ *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_TRACE2, 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 = va_arg(ap, int); ++ name = va_arg(ap, const char *); ++ aspect = va_arg(ap, const char *); ++ vtype = va_arg(ap, int); ++ pvalue= va_arg(ap, void **); ++ ++ rv = fs_get_fname(&fpath, &s_fs->s, group, name, aspect, ptemp); ++ if (APR_SUCCESS == rv) { ++ 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, int group, ++ const char *fname, apr_filetype_e ftype, apr_pool_t *p) ++{ ++ 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); ++ ++ if (APR_SUCCESS == (rv = fs_get_dname(pdir, &s_fs->s, group, name, p)) ++ && (MD_SG_NONE != group)) { ++ if (APR_SUCCESS != md_util_is_dir(*pdir, p)) { ++ if (APR_SUCCESS == (rv = apr_dir_make_recursive(*pdir, perms->dir, p))) { ++ rv = dispatch(s_fs, MD_S_FS_EV_CREATED, group, *pdir, APR_DIR, p); ++ } ++ } ++ else { ++ /* already exists */ ++ } ++ ++ if (APR_SUCCESS == rv) { ++ rv = apr_file_perms_set(*pdir, perms->dir); ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %s perm set", *pdir); ++ if (APR_STATUS_IS_ENOTIMPL(rv)) { ++ rv = APR_SUCCESS; ++ } ++ } ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %d %s", group, name); ++ return rv; ++} ++ ++ ++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 = va_arg(ap, int); ++ name = va_arg(ap, const char*); ++ aspect = va_arg(ap, const char*); ++ vtype = va_arg(ap, int); ++ value = va_arg(ap, void *); ++ create = va_arg(ap, int); ++ ++ perms = gperms(s_fs, group); ++ ++ if (APR_SUCCESS == (rv = mk_group_dir(&gdir, s_fs, group, NULL, p)) ++ && APR_SUCCESS == (rv = mk_group_dir(&dir, s_fs, group, name, p)) ++ && APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) { ++ ++ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 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; ++ ++ group = 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 (APR_SUCCESS == (rv = md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL)) ++ && APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "start remove of md %s/%s/%s", ++ groupname, name, aspect); ++ ++ if (APR_SUCCESS != (rv = 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; ++ ++ group = va_arg(ap, int); ++ name = va_arg(ap, const char*); ++ ++ groupname = md_store_group_name(group); ++ ++ if (APR_SUCCESS == (rv = 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); ++ } ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 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; ++ void *baton; ++} 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; ++ ++ 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, name, ctx->aspect, ctx->vtype, value, ptemp)) { ++ return APR_EOF; ++ } ++ } ++ 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, &ctx, p, ctx.s_fs->base, groupname, ctx.pattern, aspect, 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; ++ ++ from = va_arg(ap, int); ++ to = 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; ++ } ++ ++ rv = md_util_path_merge(&from_dir, ptemp, s_fs->base, from_group, name, NULL); ++ if (APR_SUCCESS != rv) goto out; ++ rv = md_util_path_merge(&to_dir, ptemp, s_fs->base, to_group, name, NULL); ++ if (APR_SUCCESS != rv) goto out; ++ ++ if (APR_SUCCESS != (rv = 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; ++ } ++ ++ rv = archive? md_util_is_dir(to_dir, ptemp) : APR_ENOENT; ++ if (APR_SUCCESS == rv) { ++ int n = 1; ++ const char *narch_dir; ++ ++ rv = md_util_path_merge(&dir, ptemp, s_fs->base, md_store_group_name(MD_SG_ARCHIVE), NULL); ++ if (APR_SUCCESS != rv) goto out; ++ rv = apr_dir_make_recursive(dir, MD_FPROT_D_UONLY, ptemp); ++ if (APR_SUCCESS != rv) goto out; ++ rv = md_util_path_merge(&arch_dir, ptemp, dir, name, NULL); ++ if (APR_SUCCESS != rv) goto out; ++ ++ while (1) { ++ narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n); ++ rv = apr_dir_make(narch_dir, MD_FPROT_D_UONLY, ptemp); ++ if (APR_SUCCESS == rv) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "using archive dir: %s", ++ narch_dir); ++ break; ++ } ++ else if (APR_EEXIST == rv) { ++ ++n; ++ } ++ else { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "creating archive dir: %s", ++ narch_dir); ++ goto out; ++ } ++ } ++ ++ if (APR_SUCCESS != (rv = 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 (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) { ++ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "moving %s to %s: %s", ++ from_dir, to_dir); ++ apr_file_rename(narch_dir, to_dir, ptemp); ++ goto out; ++ } ++ rv = dispatch(s_fs, MD_S_FS_EV_MOVED, to, to_dir, APR_DIR, ptemp); ++ if (APR_SUCCESS == rv) { ++ 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); ++} diff --cc modules/md/md_store_fs.h index 0000000000,0000000000..d2344be382 new file mode 100644 --- /dev/null +++ b/modules/md/md_store_fs.h @@@ -1,0 -1,0 +1,64 @@@ ++/* Copyright 2017 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 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, 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 --cc modules/md/md_util.c index 0000000000,0000000000..ac531157f0 new file mode 100644 --- /dev/null +++ b/modules/md/md_util.c @@@ -1,0 -1,0 +1,1169 @@@ ++/* Copyright 2017 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. ++ */ ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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) { ++ 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) { ++ 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; ++} ++ ++/**************************************************************************************************/ ++/* string related */ ++ ++char *md_util_str_tolower(char *s) ++{ ++ char *orig = s; ++ while (*s) { ++ *s = 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) 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) ++{ ++ return apr_file_open(pf, fn, (APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_EXCL), ++ perms, p); ++} ++ ++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_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_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); ++ 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); ++ } ++ 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); ++ 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 *); ++ ++ rv = apr_dir_open(&d, path, ptemp); ++ if (APR_SUCCESS != rv) { ++ return rv; ++ } ++ ++ while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) { ++ if (!strcmp(".", finfo.name) || !strcmp("..", finfo.name)) { ++ continue; ++ } ++ if (APR_SUCCESS == apr_fnmatch(pattern, finfo.name, 0)) { ++ if (ndepth < ctx->patterns->nelts) { ++ 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 { ++ 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; ++ ++ 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_util_is_dns_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 */ ++} ++ ++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); ++} ++ ++apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char **perr) ++{ ++ const char *s, *err = NULL; ++ apr_uri_t uri_parsed; ++ apr_status_t rv; ++ ++ if (APR_SUCCESS != (rv = apr_uri_parse(p, uri, &uri_parsed))) { ++ err = "not an uri"; ++ } ++ else if (!uri_parsed.scheme) { ++ err = "missing uri scheme"; ++ } ++ else if (strlen(uri_parsed.scheme) + 1 >= strlen(uri)) { ++ err = "missing uri identifier"; ++ } ++ else if (strchr(uri, ' ') || strchr(uri, '\t') ) { ++ err = "whitespace in uri"; ++ } ++ else if (!strncmp("http", uri_parsed.scheme, 4)) { ++ if (!uri_parsed.hostname) { ++ err = "missing hostname"; ++ } ++ else if (!md_util_is_dns_name(p, uri_parsed.hostname, 0)) { ++ err = "invalid hostname"; ++ } ++ if (uri_parsed.port_str && (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 (err) { ++ rv = APR_EINVAL; ++ } ++ *perr = err; ++ return rv; ++} ++ ++/* retry login ************************************************************************************/ ++ ++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; ++} ++ ++/* base64 url encoding ****************************************************************************/ ++ ++static const int BASE64URL_UINT6[] = { ++/* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0 */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 1 */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, /* 2 */ ++ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 3 */ ++ -1, 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, -1, -1, -1, -1, 63, /* 5 */ ++ -1, 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, -1, -1, -1, -1, -1, /* 7 */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 8 */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 9 */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* a */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* b */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* c */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* d */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* e */ ++ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* f */ ++}; ++static const 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 */ ++}; ++ ++apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, ++ apr_pool_t *pool) ++{ ++ const unsigned char *e = (const unsigned char *)encoded; ++ const unsigned char *p = e; ++ unsigned char *d; ++ int n; ++ apr_size_t len, mlen, remain, i; ++ ++ while (*p && BASE64URL_UINT6[ *p ] != -1) { ++ ++p; ++ } ++ len = p - e; ++ mlen = (len/4)*4; ++ *decoded = apr_pcalloc(pool, len+1); ++ ++ i = 0; ++ d = (unsigned char*)*decoded; ++ 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++ = n >> 16; ++ *d++ = n >> 8 & 0xffu; ++ *d++ = n & 0xffu; ++ } ++ remain = len - mlen; ++ switch (remain) { ++ case 2: ++ n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) + ++ (BASE64URL_UINT6[ e[mlen+1] ] << 12)); ++ *d++ = 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++ = n >> 16; ++ *d++ = n >> 8 & 0xffu; ++ remain = 2; ++ break; ++ default: /* do nothing */ ++ break; ++ } ++ return mlen/4*3 + remain; ++} ++ ++const char *md_util_base64url_encode(const char *data, ++ apr_size_t dlen, apr_pool_t *pool) ++{ ++ long i, len = (int)dlen; ++ apr_size_t slen = ((dlen+2)/3)*4 + 1; /* 0 terminated */ ++ const unsigned char *udata = (const unsigned char*)data; ++ char *enc, *p = apr_pcalloc(pool, slen); ++ ++ enc = p; ++ for (i = 0; i < len-2; i+= 3) { ++ *p++ = BASE64URL_CHARS[ (udata[i] >> 2) & 0x3fu ]; ++ *p++ = BASE64URL_CHARS[ ((udata[i] << 4) + (udata[i+1] >> 4)) & 0x3fu ]; ++ *p++ = BASE64URL_CHARS[ ((udata[i+1] << 2) + (udata[i+2] >> 6)) & 0x3fu ]; ++ *p++ = BASE64URL_CHARS[ udata[i+2] & 0x3fu ]; ++ } ++ ++ if (i < len) { ++ *p++ = BASE64URL_CHARS[ (udata[i] >> 2) & 0x3fu ]; ++ if (i == (len - 1)) { ++ *p++ = BASE64URL_CHARS[ (udata[i] << 4) & 0x3fu ]; ++ } ++ else { ++ *p++ = BASE64URL_CHARS[ ((udata[i] << 4) + (udata[i+1] >> 4)) & 0x3fu ]; ++ *p++ = BASE64URL_CHARS[ (udata[i+1] << 2) & 0x3fu ]; ++ } ++ } ++ *p++ = '\0'; ++ return enc; ++} ++ ++/******************************************************************************* ++ * link header handling ++ ******************************************************************************/ ++ ++typedef struct { ++ const char *s; ++ apr_size_t slen; ++ int i; ++ int link_start; ++ apr_size_t link_len; ++ int pn_start; ++ apr_size_t pn_len; ++ int 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 int find_chr(link_ctx *ctx, char c, int *pidx) ++{ ++ int 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, '\"')) { ++ int 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)) { ++ int 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, '<')) { ++ int 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)) { ++ int 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) ++{ ++ int 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; ++ int 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 ++ 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 ++ 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 = (int)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; ++} ++ diff --cc modules/md/md_util.h index 0000000000,0000000000..02c72bcf02 new file mode 100644 --- /dev/null +++ b/modules/md/md_util.h @@@ -1,0 -1,0 +1,133 @@@ ++/* Copyright 2017 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 mod_md_md_util_h ++#define mod_md_md_util_h ++ ++#include ++#include ++ ++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, ...); ++ ++/**************************************************************************************************/ ++/* string related */ ++char *md_util_str_tolower(char *s); ++ ++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); ++ ++struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src, ++ int case_sensitive); ++ ++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); ++ ++/**************************************************************************************************/ ++/* dns name check */ ++ ++int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn); ++ ++/**************************************************************************************************/ ++/* 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); ++ ++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 char *data, ++ apr_size_t len, apr_pool_t *pool); ++apr_size_t md_util_base64url_decode(const char **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); ++ ++const char *md_link_find_relation(const struct apr_table_t *headers, ++ apr_pool_t *pool, const char *relation); ++ ++/**************************************************************************************************/ ++/* 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 --cc modules/md/md_version.h index 0000000000,0000000000..60afe0f809 new file mode 100644 --- /dev/null +++ b/modules/md/md_version.h @@@ -1,0 -1,0 +1,42 @@@ ++/* Copyright 2015 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 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 "0.5.0-git" ++ ++/** ++ * @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 0x000500 ++ ++#define MD_EXPERIMENTAL 1 ++#define MD_ACME_DEF_URL "https://acme-staging.api.letsencrypt.org/directory" ++ ++#endif /* mod_md_md_version_h */ diff --cc modules/md/mod_md.c index 0000000000,0000000000..691f2fcdea new file mode 100644 --- /dev/null +++ b/modules/md/mod_md.c @@@ -1,0 -1,0 +1,970 @@@ ++/* Copyright 2015 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. ++ */ ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "md.h" ++#include "mod_md.h" ++#include "md_config.h" ++#include "md_curl.h" ++#include "md_crypt.h" ++#include "md_http.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 "md_os.h" ++#include "mod_watchdog.h" ++ ++static void md_hooks(apr_pool_t *pool); ++ ++AP_DECLARE_MODULE(md) = { ++ STANDARD20_MODULE_STUFF, ++ md_config_create_dir, /* func to create per dir config */ ++ md_config_merge_dir, /* 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 ++}; ++ ++typedef struct { ++ apr_array_header_t *mds; ++ apr_array_header_t *unused_names; ++ int can_http; ++ int can_https; ++} md_ctx; ++ ++static apr_status_t md_calc_md_list(md_ctx *ctx, apr_pool_t *p, apr_pool_t *plog, ++ apr_pool_t *ptemp, server_rec *base_server) ++{ ++ server_rec *s; ++ apr_array_header_t *mds; ++ int i, j; ++ md_t *md, *nmd; ++ const char *domain; ++ apr_status_t rv = APR_SUCCESS; ++ md_config_t *config; ++ apr_port_t effective_80, effective_443; ++ ap_listen_rec *lr; ++ apr_sockaddr_t *sa; ++ ++ ctx->can_http = 0; ++ ctx->can_https = 0; ++ mds = apr_array_make(p, 5, sizeof(const md_t*)); ++ ++ config = (md_config_t *)md_config_get(base_server); ++ effective_80 = md_config_geti(config, MD_CONFIG_LOCAL_80); ++ effective_443 = md_config_geti(config, MD_CONFIG_LOCAL_443); ++ ++ for (lr = ap_listeners; lr; lr = lr->next) { ++ for (sa = lr->bind_addr; sa; sa = sa->next) { ++ if (sa->port == effective_80 ++ && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { ++ ctx->can_http = 1; ++ } ++ else if (sa->port == effective_443 ++ && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { ++ ctx->can_https = 1; ++ } ++ } ++ } ++ ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO() ++ "server seems%s reachable via http: (port 80->%d) " ++ "and%s reachable via https: (port 443->%d) ", ++ ctx->can_http? "" : " not", effective_80, ++ ctx->can_https? "" : " not", effective_443); ++ ++ for (s = base_server; s; s = s->next) { ++ config = (md_config_t *)md_config_get(s); ++ ++ for (i = 0; i < config->mds->nelts; ++i) { ++ nmd = APR_ARRAY_IDX(config->mds, i, md_t*); ++ ++ for (j = 0; j < mds->nelts; ++j) { ++ md = APR_ARRAY_IDX(mds, j, md_t*); ++ ++ if (nmd == md) { ++ nmd = NULL; ++ break; /* merged between different configs */ ++ } ++ ++ if ((domain = md_common_name(nmd, md)) != NULL) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO() ++ "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, ++ nmd->defn_name, nmd->defn_line_number); ++ return APR_EINVAL; ++ } ++ } ++ ++ if (nmd) { ++ /* new managed domain not seen before */ ++ if (!nmd->ca_url) { ++ nmd->ca_url = md_config_gets(config, MD_CONFIG_CA_URL); ++ } ++ if (!nmd->ca_proto) { ++ nmd->ca_proto = md_config_gets(config, MD_CONFIG_CA_PROTO); ++ } ++ if (!nmd->ca_agreement) { ++ nmd->ca_agreement = md_config_gets(config, MD_CONFIG_CA_AGREEMENT); ++ } ++ if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) { ++ apr_array_clear(nmd->contacts); ++ APR_ARRAY_PUSH(nmd->contacts, const char *) = ++ md_util_schemify(p, s->server_admin, "mailto"); ++ } ++ if (nmd->drive_mode == MD_DRIVE_DEFAULT) { ++ nmd->drive_mode = md_config_geti(config, MD_CONFIG_DRIVE_MODE); ++ } ++ if (nmd->renew_window <= 0) { ++ nmd->renew_window = md_config_get_interval(config, MD_CONFIG_RENEW_WINDOW); ++ } ++ if (!nmd->ca_challenges && config->ca_challenges) { ++ nmd->ca_challenges = apr_array_copy(p, config->ca_challenges); ++ } ++ APR_ARRAY_PUSH(mds, md_t *) = nmd; ++ ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO() ++ "Added MD[%s, CA=%s, Proto=%s, Agreement=%s, Drive=%d, renew=%ld]", ++ nmd->name, nmd->ca_url, nmd->ca_proto, nmd->ca_agreement, ++ nmd->drive_mode, (long)nmd->renew_window); ++ } ++ } ++ } ++ ctx->mds = (APR_SUCCESS == rv)? mds : NULL; ++ return rv; ++} ++ ++static apr_status_t md_check_vhost_mapping(md_ctx *ctx, apr_pool_t *p, apr_pool_t *plog, ++ apr_pool_t *ptemp, server_rec *base_server) ++{ ++ server_rec *s; ++ request_rec r; ++ md_config_t *config; ++ apr_status_t rv = APR_SUCCESS; ++ md_t *md; ++ int i, j, k; ++ const char *domain, *name; ++ ++ /* Find the (at most one) managed domain for each vhost/base server and ++ * remember it at our config for it. ++ * The config is not accepted, if a vhost matches 2 or more managed domains. ++ */ ++ ctx->unused_names = apr_array_make(p, 5, sizeof(const char*)); ++ memset(&r, 0, sizeof(r)); ++ for (i = 0; i < ctx->mds->nelts; ++i) { ++ md = APR_ARRAY_IDX(ctx->mds, i, md_t*); ++ config = NULL; ++ /* This MD may apply to 0, 1 or more sever_recs */ ++ for (s = base_server; s; s = s->next) { ++ r.server = s; ++ for (j = 0; j < md->domains->nelts; ++j) { ++ domain = APR_ARRAY_IDX(md->domains, j, const char*); ++ ++ if (ap_matches_request_vhost(&r, domain, s->port)) { ++ /* Create a unique md_config_t record for this server. ++ * We keep local information here. */ ++ config = (md_config_t *)md_config_get_unique(s, p); ++ ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO() ++ "Server %s:%d matches md %s (config %s)", ++ s->server_hostname, s->port, md->name, config->name); ++ ++ if (config->md == md) { ++ /* already matched via another domain name */ ++ } ++ else if (config->md) { ++ ++ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO() ++ "conflict: MD %s matches server %s, but MD %s also matches.", ++ md->name, s->server_hostname, config->md->name); ++ rv = APR_EINVAL; ++ goto next_server; ++ } ++ ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO() ++ "Managed Domain %s applies to vhost %s:%d", md->name, ++ s->server_hostname, s->port); ++ if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) { ++ apr_array_clear(md->contacts); ++ APR_ARRAY_PUSH(md->contacts, const char *) = ++ md_util_schemify(p, s->server_admin, "mailto"); ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO() ++ "Managed Domain %s assigned server admin %s", md->name, ++ s->server_admin); ++ } ++ config->md = md; ++ ++ /* This server matches a managed domain. If it contains names or ++ * alias that are not in this md, a generated certificate will not match. */ ++ if (!md_contains(md, s->server_hostname)) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO() ++ "Virtual Host %s:%d matches Managed Domain '%s', but the name" ++ " itself is not managed. A requested MD certificate will " ++ "not match ServerName.", ++ s->server_hostname, s->port, md->name); ++ rv = APR_EINVAL; ++ goto next_server; ++ } ++ else { ++ for (k = 0; k < s->names->nelts; ++k) { ++ name = APR_ARRAY_IDX(s->names, k, const char*); ++ if (!md_contains(md, name)) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO() ++ "Virtual Host %s:%d matches Managed Domain '%s', but " ++ "the ServerAlias %s is not covered by the MD. " ++ "A requested MD certificate will not match this " ++ "alias.", s->server_hostname, s->port, md->name, ++ name); ++ rv = APR_EINVAL; ++ goto next_server; ++ } ++ } ++ } ++ goto next_server; ++ } ++ } ++next_server: ++ continue; ++ } ++ ++ if (config == NULL && md->drive_mode != MD_DRIVE_ALWAYS) { ++ /* Not an error, but looks suspicious */ ++ ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO() ++ "No VirtualHost matches Managed Domain %s", md->name); ++ APR_ARRAY_PUSH(ctx->unused_names, const char*) = md->name; ++ } ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* store & registry setup */ ++ ++static apr_status_t store_file_ev(void *baton, struct md_store_t *store, ++ md_store_fs_ev_t ev, int group, ++ const char *fname, apr_filetype_e ftype, ++ apr_pool_t *p) ++{ ++ server_rec *s = baton; ++ apr_status_t rv; ++ ++ 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 and STAGING are written to by our watchdog, ++ * running on certain mpms in a child process under a different user. Give them ++ * ownership. ++ */ ++ if (ftype == APR_DIR) { ++ switch (group) { ++ case MD_SG_CHALLENGES: ++ case MD_SG_STAGING: ++ 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, apr_pool_t *p, server_rec *s, ++ int post_config) ++{ ++ const char *base_dir; ++ md_config_t *config; ++ md_store_t *store; ++ apr_status_t rv; ++ ++ config = (md_config_t *)md_config_get(s); ++ base_dir = md_config_gets(config, MD_CONFIG_BASE_DIR); ++ base_dir = ap_server_root_relative(p, base_dir); ++ ++ if (APR_SUCCESS != (rv = md_store_fs_init(&store, p, base_dir))) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO()"setup store for %s", base_dir); ++ goto out; ++ } ++ ++ if (post_config) { ++ md_store_fs_set_event_cb(store, store_file_ev, s); ++ if (APR_SUCCESS != (rv = check_group_dir(store, MD_SG_CHALLENGES, p, s))) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() ++ "setup challenges directory"); ++ goto out; ++ } ++ if (APR_SUCCESS != (rv = check_group_dir(store, MD_SG_STAGING, p, s))) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() ++ "setup staging directory"); ++ goto out; ++ } ++ if (APR_SUCCESS != (rv = check_group_dir(store, MD_SG_ACCOUNTS, p, s))) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() ++ "setup accounts directory"); ++ goto out; ++ } ++ ++ } ++ ++ config->store = store; ++ for (s = s->next; s; s = s->next) { ++ config = (md_config_t *)md_config_get(s); ++ config->store = store; ++ } ++out: ++ *pstore = (APR_SUCCESS == rv)? store : NULL; ++ return rv; ++} ++ ++static apr_status_t setup_reg(md_reg_t **preg, apr_pool_t *p, server_rec *s, int post_config) ++{ ++ md_config_t *config; ++ apr_status_t rv; ++ ++ config = (md_config_t *)md_config_get(s); ++ if (config->store ++ || APR_SUCCESS == (rv = setup_store(&config->store, p, s, post_config))) { ++ return md_reg_init(preg, p, config->store); ++ } ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* logging setup */ ++ ++static server_rec *log_server; ++ ++static int log_is_level(void *baton, apr_pool_t *p, md_log_level_t level) ++{ ++ if (log_server) { ++ return APLOG_IS_LEVEL(log_server, 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, level, rv, log_server, "%s",buffer); ++ } ++ else { ++ ap_log_perror(file, line, APLOG_MODULE_INDEX, level, rv, p, "%s", buffer); ++ } ++ } ++} ++ ++/**************************************************************************************************/ ++/* 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); ++} ++ ++/**************************************************************************************************/ ++/* watchdog based impl. */ ++ ++#define MD_WATCHDOG_NAME "_md_" ++ ++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 { ++ apr_pool_t *p; ++ server_rec *s; ++ ap_watchdog_t *watchdog; ++ int all_valid; ++ int error_count; ++ int processed_count; ++ ++ int error_runs; ++ apr_time_t next_change; ++ ++ apr_array_header_t *mds; ++ md_reg_t *reg; ++} md_watchdog; ++ ++static apr_status_t drive_md(md_watchdog *wd, md_t *md, apr_pool_t *ptemp) ++{ ++ apr_status_t rv = APR_SUCCESS; ++ apr_time_t renew_time; ++ int errored, renew; ++ char ts[APR_RFC822_DATE_LEN]; ++ ++ if (APR_SUCCESS == (rv = md_reg_assess(wd->reg, md, &errored, &renew, wd->p))) { ++ if (errored) { ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md(%s): in error state", md->name); ++ } ++ else if (md->state == MD_S_COMPLETE && !md->expires) { ++ /* This is our indicator that we did already renew this managed domain ++ * successfully and only wait on the next restart for it to activate */ ++ ap_log_error( APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO() ++ "md(%s): has been renewed, will activate on next restart", md->name); ++ } ++ else if (renew) { ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md(%s): state=%d, driving", md->name, md->state); ++ rv = md_reg_stage(wd->reg, md, NULL, 0, ptemp); ++ if (APR_SUCCESS == rv) { ++ md->state = MD_S_COMPLETE; ++ md->expires = 0; ++ ++wd->processed_count; ++ } ++ } ++ else { ++ apr_rfc822_date(ts, md->expires); ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md(%s): is complete, cert expires %s", md->name, ts); ++ renew_time = md->expires - md->renew_window; ++ if (renew_time < wd->next_change) { ++ wd->next_change = renew_time; ++ } ++ } ++ } ++ return rv; ++} ++ ++static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) ++{ ++ md_watchdog *wd = baton; ++ apr_status_t rv = APR_SUCCESS; ++ md_t *md; ++ apr_interval_time_t interval; ++ int i; ++ ++ switch (state) { ++ case AP_WATCHDOG_STATE_STARTING: ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md watchdog start, auto drive %d mds", wd->mds->nelts); ++ break; ++ case AP_WATCHDOG_STATE_RUNNING: ++ assert(wd->reg); ++ ++ /* normally, we'd like to run at least twice a day */ ++ interval = apr_time_from_sec(MD_SECS_PER_DAY / 2); ++ ++ wd->all_valid = 1; ++ wd->processed_count = 0; ++ wd->error_count = 0; ++ wd->next_change = 0; ++ ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md watchdog run, auto drive %d mds", wd->mds->nelts); ++ ++ /* Check if all Managed Domains are ok or if we have to do something */ ++ for (i = 0; i < wd->mds->nelts; ++i) { ++ md = APR_ARRAY_IDX(wd->mds, i, md_t *); ++ if (APR_SUCCESS != (rv = drive_md(wd, md, ptemp))) { ++ wd->all_valid = 0; ++ ++wd->error_count; ++ ap_log_error( APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO() ++ "processing %s", md->name); ++ } ++ } ++ ++ /* Determine when we want to run next */ ++ wd->error_runs = wd->error_count? (wd->error_runs + 1) : 0; ++ if (wd->all_valid) { ++ ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, wd->s, "all managed domains are valid"); ++ } ++ else { ++ /* back off duration, depending on the errors we encounter in a row */ ++ interval = apr_time_from_sec(5 << (wd->error_runs - 1)); ++ if (interval > apr_time_from_sec(60*60)) { ++ interval = apr_time_from_sec(60*60); ++ } ++ ap_log_error( APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO() ++ "encountered errors for the %d. time, next run in %d seconds", ++ wd->error_runs, (int)apr_time_sec(interval)); ++ } ++ ++ /* We follow the chosen min_interval for re-evaluation, unless we ++ * know of a change (renewal) that happens before that. */ ++ if (wd->next_change) { ++ apr_interval_time_t until_next = wd->next_change - apr_time_now(); ++ if (until_next < interval) { ++ interval = until_next; ++ } ++ } ++ ++ /* Set when we'd like to be run next time. ++ * TODO: it seems that this is really only ticking down when the server ++ * runs. When you wake up a hibernated machine, the watchdog will not run right away ++ */ ++ if (APLOGdebug(wd->s)) { ++ int secs = (int)(apr_time_sec(interval) % MD_SECS_PER_DAY); ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, "next run in %2d:%02d:%02d hours", ++ (int)secs/MD_SECS_PER_HOUR, (int)(secs%(MD_SECS_PER_HOUR))/60, ++ (int)(secs%60)); ++ } ++ wd_set_interval(wd->watchdog, interval, wd, run_watchdog); ++ break; ++ case AP_WATCHDOG_STATE_STOPPING: ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md watchdog stopping"); ++ break; ++ } ++ ++ if (wd->processed_count) { ++ if (wd->all_valid) { ++ rv = md_server_graceful(ptemp, wd->s); ++ if (APR_ENOTIMPL == rv) { ++ /* self-graceful restart not supported in this setup */ ++ ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO() ++ "%d Managed Domain%s been setup and changes will be " ++ "activated on next (graceful) server restart.", ++ wd->processed_count, (wd->processed_count > 1)? "s have" : " has"); ++ } ++ } ++ else { ++ ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO() ++ "%d Managed Domain%s been setup, while %d%s " ++ "still being worked on. You may activate the changes made " ++ "by triggering a (graceful) restart at any time.", ++ wd->processed_count, (wd->processed_count > 1)? "s have" : " has", ++ wd->error_count, (wd->error_count > 1)? " are" : " is"); ++ } ++ } ++ ++ return APR_SUCCESS; ++} ++ ++static apr_status_t start_watchdog(apr_array_header_t *names, apr_pool_t *p, ++ md_reg_t *reg, server_rec *s) ++{ ++ apr_allocator_t *allocator; ++ md_watchdog *wd; ++ apr_pool_t *wdp; ++ apr_status_t rv; ++ const char *name; ++ md_t *md; ++ int i, errored, renew; ++ ++ 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() "mod_watchdog is required"); ++ return !OK; ++ } ++ ++ /* We want our own pool with own allocator to keep data across watchdog invocations */ ++ apr_allocator_create(&allocator); ++ apr_allocator_max_free_set(allocator, ap_max_mem_free); ++ rv = apr_pool_create_ex(&wdp, p, NULL, allocator); ++ if (rv != APR_SUCCESS) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() "md_watchdog: create pool"); ++ return rv; ++ } ++ apr_allocator_owner_set(allocator, wdp); ++ apr_pool_tag(wdp, "md_watchdog"); ++ ++ wd = apr_pcalloc(wdp, sizeof(*wd)); ++ wd->p = wdp; ++ wd->reg = reg; ++ wd->s = s; ++ ++ wd->mds = apr_array_make(wd->p, 10, sizeof(md_t *)); ++ for (i = 0; i < names->nelts; ++i) { ++ name = APR_ARRAY_IDX(names, i, const char *); ++ md = md_reg_get(wd->reg, name, wd->p); ++ if (md) { ++ md_reg_assess(wd->reg, md, &errored, &renew, wd->p); ++ if (errored) { ++ ap_log_error( APLOG_MARK, APLOG_WARNING, 0, wd->s, APLOGNO() ++ "md(%s): seems errored. Will not process this any further.", name); ++ } ++ else { ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() ++ "md(%s): state=%d, driving", name, md->state); ++ APR_ARRAY_PUSH(wd->mds, md_t*) = md; ++ } ++ } ++ } ++ ++ if (!wd->mds->nelts) { ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO() ++ "no managed domain in state to drive, no watchdog needed, " ++ "will check again on next server restart"); ++ apr_pool_destroy(wd->p); ++ return APR_SUCCESS; ++ } ++ ++ if (APR_SUCCESS != (rv = wd_get_instance(&wd->watchdog, MD_WATCHDOG_NAME, 0, 1, wd->p))) { ++ ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO() ++ "create md watchdog(%s)", MD_WATCHDOG_NAME); ++ return rv; ++ } ++ rv = wd_register_callback(wd->watchdog, 0, wd, run_watchdog); ++ ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO() ++ "register md watchdog(%s)", MD_WATCHDOG_NAME); ++ return rv; ++} ++ ++static void load_stage_sets(apr_array_header_t *names, apr_pool_t *p, ++ md_reg_t *reg, server_rec *s) ++{ ++ const char *name; ++ apr_status_t rv; ++ int i; ++ ++ for (i = 0; i < names->nelts; ++i) { ++ name = APR_ARRAY_IDX(names, i, const char*); ++ if (APR_SUCCESS == (rv = md_reg_load(reg, name, p))) { ++ ap_log_error( APLOG_MARK, APLOG_INFO, rv, s, APLOGNO() ++ "%s: staged set activated", name); ++ } ++ else if (!APR_STATUS_IS_ENOENT(rv)) { ++ ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() ++ "%s: error loading staged set", name); ++ } ++ } ++ return; ++} ++ ++static apr_status_t md_post_config(apr_pool_t *p, apr_pool_t *plog, ++ apr_pool_t *ptemp, server_rec *s) ++{ ++ const char *mod_md_init_key = "mod_md_init_counter"; ++ void *data = NULL; ++ md_ctx ctx; ++ apr_array_header_t *drive_names; ++ md_reg_t *reg; ++ apr_status_t rv = APR_SUCCESS; ++ const md_t *md; ++ int i; ++ ++ apr_pool_userdata_get(&data, mod_md_init_key, s->process->pool); ++ if (data == NULL) { ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO() ++ "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() ++ "mod_md (v%s), initializing...", MOD_MD_VERSION); ++ } ++ ++ init_setups(p, s); ++ memset(&ctx, 0, sizeof(ctx)); ++ ++ md_log_set(log_is_level, log_print, NULL); ++ ++ /* 1. Check uniqueness of MDs, calculate global, configured MD list. ++ * If successful, we have a list of MD definitions that do not overlap. */ ++ /* We also need to find out if we can be reached on 80/443 from the outside (e.g. the CA) */ ++ if (APR_SUCCESS != (rv = md_calc_md_list(&ctx, p, plog, ptemp, s))) { ++ goto out; ++ } ++ ++ /* 2. Check mappings of MDs to VirtulHosts defined. ++ * If successful, we have assigned MDs to server_recs in a unique way. Each server_rec ++ * config will carry 0 or 1 MD record. */ ++ if (APR_SUCCESS != (rv = md_check_vhost_mapping(&ctx, p, plog, ptemp, s))) { ++ goto out; ++ } ++ ++ /* 3. Synchronize the defintions we now have with the store via a registry (reg). */ ++ if (APR_SUCCESS != (rv = setup_reg(®, p, s, 1))) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() ++ "setup md registry"); ++ goto out; ++ } ++ if (APR_SUCCESS != (rv = md_reg_sync(reg, p, ptemp, ctx.mds, ++ ctx.can_http, ctx.can_https))) { ++ ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() ++ "synching %d mds to registry", ctx.mds->nelts); ++ goto out; ++ } ++ ++ /* Determine the managed domains that are in auto drive_mode. For those, ++ * determine in which state they are: ++ * - UNKNOWN: should not happen, report, dont drive ++ * - ERROR: something we do not know how to fix, report, dont drive ++ * - INCOMPLETE/EXPIRED: need to drive them right away ++ * - COMPLETE: determine when cert expires, drive when the time comes ++ * ++ * Start the watchdog if we have anything, now or in the future. ++ */ ++ drive_names = apr_array_make(ptemp, ctx.mds->nelts+1, sizeof(const char *)); ++ for (i = 0; i < ctx.mds->nelts; ++i) { ++ md = APR_ARRAY_IDX(ctx.mds, i, const md_t *); ++ switch (md->drive_mode) { ++ case MD_DRIVE_AUTO: ++ if (md_array_str_index(ctx.unused_names, md->name, 0, 0) >= 0) { ++ break; ++ } ++ /* fall through */ ++ case MD_DRIVE_ALWAYS: ++ APR_ARRAY_PUSH(drive_names, const char *) = md->name; ++ break; ++ default: ++ /* leave out */ ++ break; ++ } ++ } ++ ++ if (drive_names->nelts > 0) { ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO() ++ "%d out of %d mds are configured for auto-drive", ++ drive_names->nelts, ctx.mds->nelts); ++ ++ load_stage_sets(drive_names, p, reg, s); ++ md_http_use_implementation(md_curl_get_impl(p)); ++ rv = start_watchdog(drive_names, p, reg, s); ++ } ++ else { ++ ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO() ++ "no mds to auto drive, no watchdog needed"); ++ } ++out: ++ return rv; ++} ++ ++/**************************************************************************************************/ ++/* Access API to other httpd components */ ++ ++static int md_is_managed(server_rec *s) ++{ ++ md_config_t *conf = (md_config_t *)md_config_get(s); ++ ++ if (conf && conf->md) { ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO() ++ "%s: manages server %s", conf->md->name, s->server_hostname); ++ return 1; ++ } ++ ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, ++ "server %s is not managed", s->server_hostname); ++ return 0; ++} ++ ++static apr_status_t md_get_credentials(server_rec *s, apr_pool_t *p, ++ const char **pkeyfile, const char **pcertfile, ++ const char **pchainfile) ++{ ++ apr_status_t rv = APR_ENOENT; ++ md_config_t *conf; ++ md_reg_t *reg; ++ const md_t *md; ++ ++ *pkeyfile = NULL; ++ *pcertfile = NULL; ++ *pchainfile = NULL; ++ conf = (md_config_t *)md_config_get(s); ++ ++ if (conf && conf->md && conf->store) { ++ if (APR_SUCCESS == (rv = md_reg_init(®, p, conf->store))) { ++ md = md_reg_get(reg, conf->md->name, p); ++ if (md->state != MD_S_COMPLETE) { ++ return APR_EAGAIN; ++ } ++ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO() ++ "%s: loading credentials for server %s", md->name, s->server_hostname); ++ return md_reg_get_cred_files(reg, md, p, pkeyfile, pcertfile, pchainfile); ++ } ++ } ++ return rv; ++} ++ ++ ++static int md_is_challenge(conn_rec *c, const char *servername, ++ X509 **pcert, EVP_PKEY **pkey) ++{ ++ md_config_t *conf; ++ apr_size_t slen, sufflen = sizeof(MD_TLSSNI01_DNS_SUFFIX) - 1; ++ apr_status_t rv; ++ ++ slen = strlen(servername); ++ if (slen <= sufflen ++ || apr_strnatcasecmp(MD_TLSSNI01_DNS_SUFFIX, servername + slen - sufflen)) { ++ return 0; ++ } ++ ++ conf = (md_config_t *)md_config_get(c->base_server); ++ if (conf && conf->store) { ++ md_store_t *store = conf->store; ++ md_cert_t *mdcert; ++ md_pkey_t *mdpkey; ++ ++ rv = md_store_load(store, MD_SG_CHALLENGES, servername, ++ MD_FN_TLSSNI01_CERT, MD_SV_CERT, (void**)&mdcert, c->pool); ++ if (APR_SUCCESS == rv && (*pcert = md_cert_get_X509(mdcert))) { ++ rv = md_store_load(store, MD_SG_CHALLENGES, servername, ++ MD_FN_TLSSNI01_PKEY, MD_SV_PKEY, (void**)&mdpkey, c->pool); ++ if (APR_SUCCESS == rv && (*pkey = md_pkey_get_EVP_PKEY(mdpkey))) { ++ ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, c, APLOGNO() ++ "%s: is a tls-sni-01 challenge host", servername); ++ return 1; ++ } ++ ap_log_cerror(APLOG_MARK, APLOG_WARNING, rv, c, APLOGNO() ++ "%s: challenge data not complete, key unavailable", servername); ++ } ++ else { ++ ap_log_cerror(APLOG_MARK, APLOG_INFO, rv, c, APLOGNO() ++ "%s: unknown TLS SNI challenge host", servername); ++ } ++ } ++ *pcert = NULL; ++ *pkey = NULL; ++ return 0; ++} ++ ++/**************************************************************************************************/ ++/* ACME challenge responses */ ++ ++#define ACME_CHALLENGE_PREFIX "/.well-known/acme-challenge/" ++ ++static int md_http_challenge_pr(request_rec *r) ++{ ++ apr_bucket_brigade *bb; ++ const md_config_t *conf; ++ const char *base_dir, *name, *data; ++ apr_status_t rv; ++ ++ if (!strncmp(ACME_CHALLENGE_PREFIX, r->parsed_uri.path, sizeof(ACME_CHALLENGE_PREFIX)-1)) { ++ if (r->method_number == M_GET) { ++ md_store_t *store; ++ ++ conf = ap_get_module_config(r->server->module_config, &md_module); ++ store = conf->store; ++ ++ base_dir = md_config_gets(conf, MD_CONFIG_BASE_DIR); ++ name = r->parsed_uri.path + sizeof(ACME_CHALLENGE_PREFIX)-1; ++ ++ r->status = HTTP_NOT_FOUND; ++ if (!ap_strchr_c(name, '/') && store) { ++ base_dir = ap_server_root_relative(r->pool, base_dir); ++ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, ++ "Challenge for %s (%s)", r->hostname, r->uri); ++ ++ rv = md_store_load(store, MD_SG_CHALLENGES, r->hostname, ++ MD_FN_HTTP01, MD_SV_TEXT, (void**)&data, r->pool); ++ if (APR_SUCCESS == rv) { ++ apr_size_t len = strlen(data); ++ ++ 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); ++ } ++ else if (APR_STATUS_IS_ENOENT(rv)) { ++ return HTTP_NOT_FOUND; ++ } ++ else if (APR_ENOENT != rv) { ++ ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO() ++ "loading challenge %s from store %s", name, base_dir); ++ return HTTP_INTERNAL_SERVER_ERROR; ++ } ++ } ++ return r->status; ++ } ++ else { ++ return HTTP_NOT_IMPLEMENTED; ++ } ++ } ++ return DECLINED; ++} ++ ++/* Runs once per created child process. Perform any process ++ * related initionalization here. ++ */ ++static void md_child_init(apr_pool_t *pool, server_rec *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", NULL}; ++ ++ md_acme_init(pool, AP_SERVER_BASEVERSION); ++ ++ ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks"); ++ ++ /* Run once after configuration is set, before mod_ssl. ++ */ ++ ap_hook_post_config(md_post_config, NULL, mod_ssl, APR_HOOK_MIDDLE); ++ ++ /* 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_http_challenge_pr, NULL, NULL, APR_HOOK_MIDDLE); ++ ++ APR_REGISTER_OPTIONAL_FN(md_is_managed); ++ APR_REGISTER_OPTIONAL_FN(md_get_credentials); ++ APR_REGISTER_OPTIONAL_FN(md_is_challenge); ++} diff --cc modules/md/mod_md.h index 0000000000,0000000000..40e4cd2c01 new file mode 100644 --- /dev/null +++ b/modules/md/mod_md.h @@@ -1,0 -1,0 +1,38 @@@ ++/* Copyright 2015 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 mod_md_mod_md_h ++#define mod_md_mod_md_h ++ ++#include ++#include ++ ++struct server_rec; ++ ++APR_DECLARE_OPTIONAL_FN(int, ++ md_is_managed, (struct server_rec *)); ++ ++APR_DECLARE_OPTIONAL_FN(apr_status_t, ++ md_get_credentials, (struct server_rec *, apr_pool_t *, ++ const char **pkeyfile, ++ const char **pcertfile, ++ const char **pchainfile)); ++ ++APR_DECLARE_OPTIONAL_FN(int, ++ md_is_challenge, (struct conn_rec *, const char *, ++ X509 **pcert, EVP_PKEY **pkey)); ++ ++ ++#endif /* mod_md_mod_md_h */