From: Stefan Eissing Date: Wed, 27 Apr 2022 12:08:18 +0000 (+0000) Subject: Merge /httpd/httpd/trunk:r1898962,1900039,1900145,1900313-1900314 X-Git-Tag: 2.4.54-rc1-candidate~105 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cc6d2949f6fed15199c09e85ad110a13aa7a7304;p=thirdparty%2Fapache%2Fhttpd.git Merge /httpd/httpd/trunk:r1898962,1900039,1900145,1900313-1900314 Backport of all recent changes to experimental mod_md. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1900316 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/CMakeLists.txt b/CMakeLists.txt index ef8da3f31cd..29949a34d00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -489,7 +489,7 @@ SET(mod_md_extra_sources modules/md/md_ocsp.c modules/md/md_util.c modules/md/mod_md_config.c modules/md/mod_md_drive.c modules/md/mod_md_os.c modules/md/mod_md_status.c - modules/md/mod_md_ocsp.c + modules/md/mod_md_ocsp.c modules/md/md_tailscale.c ) SET(mod_optional_hook_export_extra_defines AP_DECLARE_EXPORT) # bogus reuse of core API prefix SET(mod_proxy_extra_defines PROXY_DECLARE_EXPORT) diff --git a/changes-entries/md_auto_status.txt b/changes-entries/md_auto_status.txt new file mode 100644 index 00000000000..34faddd4ce1 --- /dev/null +++ b/changes-entries/md_auto_status.txt @@ -0,0 +1,5 @@ + * Implement full auto status ("key: value" type status output). + Especially not only status summary counts for certificates and + OCSP stapling but also lists. Auto status format is similar to + what was used for mod_proxy_balancer. + [Rainer Jung] diff --git a/changes-entries/md_tailscale.txt b/changes-entries/md_tailscale.txt new file mode 100644 index 00000000000..e30aba69ca6 --- /dev/null +++ b/changes-entries/md_tailscale.txt @@ -0,0 +1,5 @@ + *) mod_md: added support for managing certificates via a + local tailscale demon for users of that secure networking. + This gives trusted certificates for tailscale assigned + domain names in the *.ts.net space. + [Stefan Eissing] \ No newline at end of file diff --git a/changes-entries/md_timeperiod_null.txt b/changes-entries/md_timeperiod_null.txt new file mode 100644 index 00000000000..5beb6c8f52d --- /dev/null +++ b/changes-entries/md_timeperiod_null.txt @@ -0,0 +1,4 @@ + *) mod_md: a possible NULL pointer deref was fixed in + the JSON code for persisting time periods (start+end). + Fixes #282 on mod_md's github. + Thanks to @marcstern for finding this. diff --git a/docs/manual/mod/mod_md.xml b/docs/manual/mod/mod_md.xml index ed2dc6cd922..800abbaa2fd 100644 --- a/docs/manual/mod/mod_md.xml +++ b/docs/manual/mod/mod_md.xml @@ -285,6 +285,44 @@ MDChallengeDns01 /usr/bin/acme-setup-dns

+ tailscale +

+ Since version 2.4.14 of the module, you can use it to get certificates + for your tailscale domains. +

+ +<MDomain mydomain.some-thing.ts.net> + MDCertificateProtocol tailscale + MDCertificateAuthority file://localhost/var/run/tailscale/tailscaled.sock", +</MDomain> + +

+ Tailscale provides secure networking between your machines, where ever + they are, and can provide domain names in the *.ts.net space for them. + For those, it will then provide Let's Encrypt certificates as well, so + you can open these domains in your browser securely. +

+

+ The directives listed above tell Apache to contact the local tailscale + demon for obtaining and renewing certificates. This will only work for + the domain name that tailscale assigns to your machine. +

+

+ Otherwise, these certificates work exactly like the ones retrieved + via the ACME protocol from Lets Encrypt. You see them in status reporting + and MDMessageCmd directives are executed for them as well. +

+

+ More details are + available at the mod_md github documentation. +

+

+ Note that this feature only works on machines where the tailscale + demon provides a unix domain socket. This, so far, seems only the + case on *nix systems. +

+
+ diff --git a/modules/md/config2.m4 b/modules/md/config2.m4 index bcce501633a..11d4f32dbd9 100644 --- a/modules/md/config2.m4 +++ b/modules/md/config2.m4 @@ -264,6 +264,7 @@ md_reg.lo dnl md_status.lo dnl md_store.lo dnl md_store_fs.lo dnl +md_tailscale.lo dnl md_time.lo dnl md_util.lo dnl mod_md.lo dnl diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index 6c461dc9af3..bc0f17f2710 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -1036,9 +1036,19 @@ static apr_status_t acme_driver_preload(md_proto_driver_t *d, return rv; } +static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p) +{ + (void)p; + if (!md->ca_url) { + md->ca_url = MD_ACME_DEF_URL; + } + return APR_SUCCESS; +} + static md_proto_t ACME_PROTO = { MD_PROTO_ACME, acme_driver_init, acme_driver_renew, - acme_driver_preload_init, acme_driver_preload + acme_driver_preload_init, acme_driver_preload, + acme_complete_md, }; apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) diff --git a/modules/md/md_core.c b/modules/md/md_core.c index 620b809984c..f82f950503a 100644 --- a/modules/md/md_core.c +++ b/modules/md/md_core.c @@ -427,7 +427,7 @@ apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const cha } } *purl = name; - rv = md_util_abs_http_uri_check(p, name, &err); + rv = md_util_abs_uri_check(p, name, &err); if (APR_SUCCESS != rv) { apr_array_header_t *names; diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c index 73e89009775..8baab51c8b3 100644 --- a/modules/md/md_crypt.c +++ b/modules/md/md_crypt.c @@ -709,6 +709,53 @@ apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, return rv; } +apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool, + const struct md_http_response_t *res) +{ + apr_status_t rv; + apr_off_t data_len; + char *pem_data; + apr_size_t pem_len; + md_pkey_t *pkey; + BIO *bf; + passwd_ctx ctx; + + rv = apr_brigade_length(res->body, 1, &data_len); + if (APR_SUCCESS != rv) goto leave; + if (data_len > 1024*1024) { /* certs usually are <2k each */ + rv = APR_EINVAL; + goto leave; + } + rv = apr_brigade_pflatten(res->body, &pem_data, &pem_len, res->req->pool); + if (APR_SUCCESS != rv) goto leave; + + if (NULL == (bf = BIO_new_mem_buf(pem_data, (int)pem_len))) { + rv = APR_ENOMEM; + goto leave; + } + pkey = make_pkey(pool); + ctx.pass_phrase = NULL; + ctx.pass_len = 0; + ERR_clear_error(); + pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, NULL, &ctx); + BIO_free(bf); + + if (pkey->pkey == NULL) { + unsigned long err = ERR_get_error(); + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, pool, + "error loading pkey from http response: %s", + ERR_error_string(err, NULL)); + goto leave; + } + rv = APR_SUCCESS; + apr_pool_cleanup_register(pool, pkey, pkey_cleanup, apr_pool_cleanup_null); + +leave: + *ppkey = (APR_SUCCESS == rv)? pkey : NULL; + return rv; +} + /* Determine the message digest used for signing with the given private key. */ static const EVP_MD *pkey_get_MD(md_pkey_t *pkey) @@ -1137,6 +1184,11 @@ const char *md_cert_get_serial_number(const md_cert_t *cert, apr_pool_t *p) return s; } +int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b) +{ + return X509_cmp(a->x509, b->x509) == 0; +} + int md_cert_is_valid_now(const md_cert_t *cert) { return ((X509_cmp_current_time(X509_get_notBefore(cert->x509)) < 0) diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h index 203dc40f409..a892e00f1e6 100644 --- a/modules/md/md_crypt.h +++ b/modules/md/md_crypt.h @@ -117,6 +117,12 @@ void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey); apr_status_t md_crypt_hmac64(const char **pmac64, const struct md_data_t *hmac_key, apr_pool_t *p, const char *d, size_t dlen); +/** + * Read a private key from a http response. + */ +apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool, + const struct md_http_response_t *res); + /**************************************************************************************************/ /* X509 certificates */ @@ -179,6 +185,11 @@ apr_time_t md_cert_get_not_after(const md_cert_t *cert); apr_time_t md_cert_get_not_before(const md_cert_t *cert); struct md_timeperiod_t md_cert_get_valid(const md_cert_t *cert); +/** + * Return != 0 iff the hash values of the certificates are equal. + */ +int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b); + apr_status_t md_cert_get_issuers_uri(const char **puri, const md_cert_t *cert, apr_pool_t *p); apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, const md_cert_t *cert, apr_pool_t *p); diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c index e05f37b8997..e3f32d35485 100644 --- a/modules/md/md_curl.c +++ b/modules/md/md_curl.c @@ -301,6 +301,9 @@ static apr_status_t internals_setup(md_http_request_t *req) if (req->ca_file) { curl_easy_setopt(curl, CURLOPT_CAINFO, req->ca_file); } + if (req->unix_socket_path) { + curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, req->unix_socket_path); + } if (req->body_len >= 0) { /* set the Content-Length */ diff --git a/modules/md/md_http.c b/modules/md/md_http.c index 53e4f89d99f..74db961301e 100644 --- a/modules/md/md_http.c +++ b/modules/md/md_http.c @@ -33,6 +33,7 @@ struct md_http_t { void *impl_data; /* to be used by the implementation */ const char *user_agent; const char *proxy_url; + const char *unix_socket_path; md_http_timeouts_t timeout; const char *ca_file; }; @@ -143,6 +144,11 @@ void md_http_set_ca_file(md_http_t *http, const char *ca_file) http->ca_file = ca_file; } +void md_http_set_unix_socket_path(md_http_t *http, const char *path) +{ + http->unix_socket_path = path; +} + static apr_status_t req_set_body(md_http_request_t *req, const char *content_type, apr_bucket_brigade *body, apr_off_t body_len, int detect_len) @@ -211,6 +217,7 @@ static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, req->proxy_url = http->proxy_url; req->timeout = http->timeout; req->ca_file = http->ca_file; + req->unix_socket_path = http->unix_socket_path; *preq = req; return rv; } diff --git a/modules/md/md_http.h b/modules/md/md_http.h index e24de031139..c210aa99130 100644 --- a/modules/md/md_http.h +++ b/modules/md/md_http.h @@ -65,6 +65,7 @@ struct md_http_request_t { const char *user_agent; const char *proxy_url; const char *ca_file; + const char *unix_socket_path; apr_table_t *headers; struct apr_bucket_brigade *body; apr_off_t body_len; @@ -117,6 +118,12 @@ void md_http_set_stalling(md_http_request_t *req, long bytes_per_sec, apr_time_t */ void md_http_set_ca_file(md_http_t *http, const char *ca_file); +/** + * Set the path of a unix domain socket for use instead of TCP + * in a connection. Disable by providing NULL as path. + */ +void md_http_set_unix_socket_path(md_http_t *http, const char *path); + /** * Perform the request. Then this function returns, the request and * all its memory has been freed and must no longer be used. diff --git a/modules/md/md_json.c b/modules/md/md_json.c index 52187168cd9..e0f977ea564 100644 --- a/modules/md/md_json.c +++ b/modules/md/md_json.c @@ -176,7 +176,7 @@ static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap) aj = json_object_get(j, key); if (!aj) { aj = json_array(); - json_object_set(j, key, aj); + json_object_set_new(j, key, aj); } if (!json_is_array(aj)) { @@ -202,7 +202,7 @@ static apr_status_t jselect_insert(json_t *val, size_t index, md_json_t *json, v aj = json_object_get(j, key); if (!aj) { aj = json_array(); - json_object_set(j, key, aj); + json_object_set_new(j, key, aj); } if (!json_is_array(aj)) { @@ -1264,7 +1264,7 @@ apr_status_t md_json_set_timeperiod(const md_timeperiod_t *tp, md_json_t *json, const char *key; apr_status_t rv; - if (!tp || tp->start || tp->end) { + if (tp && tp->start && tp->end) { jn = json_object(); apr_rfc822_date(ts, tp->start); json_object_set_new(jn, "from", json_string(ts)); diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c index 4fba9c7ae45..0c59aeb737d 100644 --- a/modules/md/md_reg.c +++ b/modules/md/md_reg.c @@ -33,6 +33,7 @@ #include "md_reg.h" #include "md_store.h" #include "md_status.h" +#include "md_tailscale.h" #include "md_util.h" #include "md_acme.h" @@ -98,7 +99,8 @@ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *st md_timeslice_create(®->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF); md_timeslice_create(®->warn_window, p, MD_TIME_LIFE_NORM, MD_TIME_WARN_WINDOW_DEF); - if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))) { + if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p)) + && APR_SUCCESS == (rv = md_tailscale_protos_add(reg->protos, p))) { rv = load_props(reg, p); } @@ -901,12 +903,22 @@ apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool md_t *old; apr_status_t rv; int changed = 1; + md_proto_t *proto; - if (!md->ca_url) { - md->ca_url = MD_ACME_DEF_URL; - md->ca_proto = MD_PROTO_ACME; + if (!md->ca_proto) { + md->ca_proto = MD_PROTO_ACME; + } + proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto)); + if (!proto) { + rv = APR_ENOTIMPL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, + "[%s] uses unknown CA protocol '%s'", + md->name, md->ca_proto); + goto leave; } - + rv = proto->complete_md(md, p); + if (APR_SUCCESS != rv) goto leave; + rv = state_init(reg, p, md); if (APR_SUCCESS != rv) goto leave; diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h index 46034b0fe68..aa626c92768 100644 --- a/modules/md/md_reg.h +++ b/modules/md/md_reg.h @@ -220,6 +220,7 @@ typedef apr_status_t md_proto_renew_cb(md_proto_driver_t *driver, struct md_resu typedef apr_status_t md_proto_init_preload_cb(md_proto_driver_t *driver, struct md_result_t *result); typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver, md_store_group_t group, struct md_result_t *result); +typedef apr_status_t md_proto_complete_md_cb(md_t *md, apr_pool_t *p); struct md_proto_t { const char *protocol; @@ -227,6 +228,7 @@ struct md_proto_t { md_proto_renew_cb *renew; md_proto_init_preload_cb *init_preload; md_proto_preload_cb *preload; + md_proto_complete_md_cb *complete_md; }; /** diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c index ab43054ce58..c2a4a4e4930 100644 --- a/modules/md/md_store_fs.c +++ b/modules/md/md_store_fs.c @@ -503,6 +503,7 @@ static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs, perms = gperms(s_fs, group); + *pdir = NULL; rv = fs_get_dname(pdir, &s_fs->s, group, name, p); if ((APR_SUCCESS != rv) || (MD_SG_NONE == group)) goto cleanup; @@ -521,7 +522,8 @@ static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs, } cleanup: if (APR_SUCCESS != rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "mk_group_dir %d %s", group, name); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "mk_group_dir %d %s", + group, (*pdir? *pdir : (name? name : "(null)"))); } return rv; } diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c new file mode 100644 index 00000000000..dd3b1458ad3 --- /dev/null +++ b/modules/md/md_tailscale.c @@ -0,0 +1,381 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#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_result.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" + +#include "md_tailscale.h" + +typedef struct { + apr_pool_t *pool; + md_proto_driver_t *driver; + const char *unix_socket_path; + md_t *md; + apr_array_header_t *chain; + md_pkey_t *pkey; +} ts_ctx_t; + +static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result) +{ + ts_ctx_t *ts_ctx; + apr_uri_t uri; + const char *ca_url; + apr_status_t rv = APR_SUCCESS; + + md_result_set(result, APR_SUCCESS, NULL); + ts_ctx = apr_pcalloc(d->p, sizeof(*ts_ctx)); + ts_ctx->pool = d->p; + ts_ctx->driver = d; + ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *)); + + ca_url = d->md->ca_url; + if (!ca_url) { + ca_url = MD_TAILSCALE_DEF_URL; + } + rv = apr_uri_parse(d->p, ca_url, &uri); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "error parsing CA URL `%s`", ca_url); + goto leave; + } + if (uri.scheme && uri.scheme[0] && strcmp("file", uri.scheme)) { + rv = APR_ENOTIMPL; + md_result_printf(result, rv, "non `file` URLs not supported, CA URL is `%s`", + ca_url); + goto leave; + } + if (uri.hostname && uri.hostname[0] && strcmp("localhost", uri.hostname)) { + rv = APR_ENOTIMPL; + md_result_printf(result, rv, "non `localhost` URLs not supported, CA URL is `%s`", + ca_url); + goto leave; + } + ts_ctx->unix_socket_path = uri.path; + d->baton = ts_ctx; + +leave: + return rv; +} + +static apr_status_t ts_preload_init(md_proto_driver_t *d, md_result_t *result) +{ + return ts_init(d, result); +} + +static apr_status_t ts_preload(md_proto_driver_t *d, + md_store_group_t load_group, md_result_t *result) +{ + apr_status_t rv; + md_t *md; + md_credentials_t *creds; + md_pkey_spec_t *pkspec; + apr_array_header_t *all_creds; + const char *name; + int i; + + name = d->md->name; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name); + /* Load data from MD_SG_STAGING and save it into "load_group". + */ + if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) { + md_result_set(result, rv, "loading staged md.json"); + goto leave; + } + + /* tailscale generates one cert+key with key specification being whatever + * it chooses. Use the NULL spec here. + */ + all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*)); + pkspec = NULL; + if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) { + md_result_printf(result, rv, "loading staged credentials"); + goto leave; + } + if (!creds->chain) { + rv = APR_ENOENT; + md_result_printf(result, rv, "no certificate in staged credentials"); + goto leave; + } + if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) { + md_result_printf(result, rv, "certificate and private key do not match in staged credentials"); + goto leave; + } + APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds; + + md_result_activity_setn(result, "purging store tmp space"); + rv = md_store_purge(d->store, d->p, load_group, name); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, NULL); + goto leave; + } + + md_result_activity_setn(result, "saving staged md/privkey/pubcert"); + if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) { + md_result_set(result, rv, "writing md.json"); + goto leave; + } + + for (i = 0; i < all_creds->nelts; ++i) { + creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*); + if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) { + md_result_printf(result, rv, "writing credentials #%d", i); + goto leave; + } + } + + md_result_set(result, APR_SUCCESS, "saved staged data successfully"); + +leave: + md_result_log(result, MD_LOG_DEBUG); + return rv; +} + +static apr_status_t rv_of_response(const md_http_response_t *res) +{ + switch (res->status) { + case 200: + return APR_SUCCESS; + case 400: + return APR_EINVAL; + case 401: /* sectigo returns this instead of 403 */ + case 403: + return APR_EACCES; + case 404: + return APR_ENOENT; + default: + return APR_EGENERAL; + } + return APR_SUCCESS; +} + +static apr_status_t on_get_cert(const md_http_response_t *res, void *baton) +{ + ts_ctx_t *ts_ctx = baton; + apr_status_t rv; + + rv = rv_of_response(res); + if (APR_SUCCESS != rv) goto leave; + apr_array_clear(ts_ctx->chain); + rv = md_cert_chain_read_http(ts_ctx->chain, ts_ctx->pool, res); + if (APR_SUCCESS != rv) goto leave; + +leave: + return rv; +} + +static apr_status_t on_get_key(const md_http_response_t *res, void *baton) +{ + ts_ctx_t *ts_ctx = baton; + apr_status_t rv; + + rv = rv_of_response(res); + if (APR_SUCCESS != rv) goto leave; + rv = md_pkey_read_http(&ts_ctx->pkey, ts_ctx->pool, res); + if (APR_SUCCESS != rv) goto leave; + +leave: + return rv; +} + +static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result) +{ + const char *name, *domain, *url; + apr_status_t rv = APR_ENOENT; + ts_ctx_t *ts_ctx = d->baton; + md_http_t *http; + const md_pubcert_t *pubcert; + md_cert_t *old_cert, *new_cert; + int reset_staging = d->reset; + + /* "renewing" the certificate from tailscale. Since tailscale has its + * own ideas on when to do this, we can only inspect the certificate + * it gives us and see if it is different from the current one we have. + * (if we have any. first time, lacking a cert, any it gives us is + * considered as 'renewed'.) + */ + name = d->md->name; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: renewing cert", name); + + /* When not explicitly told to reset, we check the existing data. If + * it is incomplete or old, we trigger the reset for a clean start. */ + if (!reset_staging) { + md_result_activity_setn(result, "Checking staging area"); + rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ts_ctx->md, d->p); + if (APR_SUCCESS == rv) { + /* So, we have a copy in staging, but is it a recent or an old one? */ + if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) { + reset_staging = 1; + } + } + else if (APR_STATUS_IS_ENOENT(rv)) { + reset_staging = 1; + rv = APR_SUCCESS; + } + } + + if (reset_staging) { + md_result_activity_setn(result, "Resetting staging area"); + /* reset the staging area for this domain */ + rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: reset staging area", d->md->name); + if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { + md_result_printf(result, rv, "resetting staging area"); + goto leave; + } + rv = APR_SUCCESS; + ts_ctx->md = NULL; + } + + if (!ts_ctx->md || strcmp(ts_ctx->md->ca_url, d->md->ca_url)) { + md_result_activity_printf(result, "Resetting staging for %s", d->md->name); + /* re-initialize staging */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name); + md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + ts_ctx->md = md_copy(d->p, d->md); + rv = md_save(d->store, d->p, MD_SG_STAGING, ts_ctx->md, 0); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Saving MD information in staging area."); + md_result_log(result, MD_LOG_ERR); + goto leave; + } + } + + if (!ts_ctx->unix_socket_path) { + rv = APR_ENOTIMPL; + md_result_set(result, rv, "only unix sockets are supported for tailscale connections"); + goto leave; + } + + rv = md_util_is_unix_socket(ts_ctx->unix_socket_path, d->p); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "tailscale socket not available, may not be up: %s", + ts_ctx->unix_socket_path); + goto leave; + } + + rv = md_http_create(&http, d->p, + apr_psprintf(d->p, "Apache mod_md/%s", MOD_MD_VERSION), + NULL); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "creating http context"); + goto leave; + } + md_http_set_unix_socket_path(http, ts_ctx->unix_socket_path); + + domain = (d->md->domains->nelts > 0)? + APR_ARRAY_IDX(d->md->domains, 0, const char*) : NULL; + if (!domain) { + rv = APR_EINVAL; + md_result_set(result, rv, "no domain names available"); + } + + url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=crt", + domain); + rv = md_http_GET_perform(http, url, NULL, on_get_cert, ts_ctx); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "retrieving certificate from tailscale"); + goto leave; + } + if (ts_ctx->chain->nelts <= 0) { + rv = APR_ENOENT; + md_result_set(result, rv, "tailscale returned no certificates"); + goto leave; + } + + /* Got the key and the chain, is it new? */ + rv = md_reg_get_pubcert(&pubcert, d->reg,d->md, 0, d->p); + if (APR_SUCCESS == rv) { + old_cert = APR_ARRAY_IDX(pubcert->certs, 0, md_cert_t*); + new_cert = APR_ARRAY_IDX(ts_ctx->chain, 0, md_cert_t*); + if (md_certs_are_equal(old_cert, new_cert)) { + /* tailscale has not renewed the certificate, yet */ + rv = APR_ENOENT; + md_result_set(result, rv, "tailscale has not renewed the certificate yet"); + /* let's check this daily */ + md_result_delay_set(result, apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY)); + goto leave; + } + } + + /* We have a new certificate (or had none before). + * Get the key and store both in STAGING. + */ + url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=key", + domain); + rv = md_http_GET_perform(http, url, NULL, on_get_key, ts_ctx); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "retrieving key from tailscale"); + goto leave; + } + + rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, name, NULL, ts_ctx->pkey, 1); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "saving private key"); + goto leave; + } + + rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, name, + NULL, ts_ctx->chain, 1); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "saving new certificate chain."); + goto leave; + } + + md_result_set(result, APR_SUCCESS, + "A new tailscale certificate has been retrieved successfully and can " + "be used. A graceful server restart is recommended."); + +leave: + md_result_log(result, MD_LOG_DEBUG); + return rv; +} + +static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p) +{ + (void)p; + if (!md->ca_url) { + md->ca_url = MD_TAILSCALE_DEF_URL; + } + return APR_SUCCESS; +} + + +static md_proto_t TAILSCALE_PROTO = { + MD_PROTO_TAILSCALE, ts_init, ts_renew, + ts_preload_init, ts_preload, ts_complete_md, +}; + +apr_status_t md_tailscale_protos_add(apr_hash_t *protos, apr_pool_t *p) +{ + (void)p; + apr_hash_set(protos, MD_PROTO_TAILSCALE, sizeof(MD_PROTO_TAILSCALE)-1, &TAILSCALE_PROTO); + return APR_SUCCESS; +} diff --git a/modules/md/md_tailscale.h b/modules/md/md_tailscale.h new file mode 100644 index 00000000000..67a874dc28c --- /dev/null +++ b/modules/md/md_tailscale.h @@ -0,0 +1,25 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef mod_md_md_tailscale_h +#define mod_md_md_tailscale_h + +#define MD_PROTO_TAILSCALE "tailscale" + +apr_status_t md_tailscale_protos_add(struct apr_hash_t *protos, apr_pool_t *p); + +#endif /* mod_md_md_tailscale_h */ + diff --git a/modules/md/md_util.c b/modules/md/md_util.c index 23923c8b2ee..884c0bb91e8 100644 --- a/modules/md/md_util.c +++ b/modules/md/md_util.c @@ -398,6 +398,16 @@ apr_status_t md_util_is_file(const char *path, apr_pool_t *pool) return rv; } +apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool) +{ + apr_finfo_t info; + apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool); + if (rv == APR_SUCCESS) { + rv = (info.filetype == APR_SOCK)? APR_SUCCESS : APR_EINVAL; + } + return rv; +} + int md_file_exists(const char *fname, apr_pool_t *p) { return (fname && *fname && APR_SUCCESS == md_util_is_file(fname, p)); diff --git a/modules/md/md_util.h b/modules/md/md_util.h index 71e66eb500c..e430655fca5 100644 --- a/modules/md/md_util.h +++ b/modules/md/md_util.h @@ -189,6 +189,7 @@ apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...); apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool); apr_status_t md_util_is_file(const char *path, apr_pool_t *pool); +apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool); int md_file_exists(const char *fname, apr_pool_t *p); typedef apr_status_t md_util_file_cb(void *baton, struct apr_file_t *f, apr_pool_t *p); diff --git a/modules/md/md_version.h b/modules/md/md_version.h index ae723f621ff..92cad1b1cdc 100644 --- a/modules/md/md_version.h +++ b/modules/md/md_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the md module as c string */ -#define MOD_MD_VERSION "2.4.10" +#define MOD_MD_VERSION "2.4.14" /** * @macro @@ -35,8 +35,9 @@ * 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 0x02040a +#define MOD_MD_VERSION_NUM 0x02040e -#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory" +#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory" +#define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock" #endif /* mod_md_md_version_h */ diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp index e141ffc10f7..d99fb1ccc46 100644 --- a/modules/md/mod_md.dsp +++ b/modules/md/mod_md.dsp @@ -205,6 +205,10 @@ SOURCE=./md_store_fs.c # End Source File # Begin Source File +SOURCE=./md_tailscale.c +# End Source File +# Begin Source File + SOURCE=./md_time.c # End Source File # Begin Source File diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c index 8d3260634ec..82c7191768d 100644 --- a/modules/md/mod_md_config.c +++ b/modules/md/mod_md_config.c @@ -26,6 +26,7 @@ #include #include "md.h" +#include "md_acme.h" #include "md_crypt.h" #include "md_log.h" #include "md_json.h" @@ -108,7 +109,7 @@ static md_srv_conf_t defconf = { &def_warn_window, /* warn window */ NULL, /* ca url */ NULL, /* ca contact (email) */ - "ACME", /* ca protocol */ + MD_PROTO_ACME, /* ca protocol */ NULL, /* ca agreemnent */ NULL, /* ca challenges array */ NULL, /* ca eab kid */ diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c index 390290b85cd..6891ef832ee 100644 --- a/modules/md/mod_md_status.c +++ b/modules/md/mod_md_status.c @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + #include #include #include @@ -55,6 +55,7 @@ #define APACHE_PREFIX "/.httpd/" #define MD_STATUS_RESOURCE APACHE_PREFIX"certificate-status" +#define HTML_STATUS(X) (!((X)->flags & AP_STATUS_SHORT)) int md_http_cert_status(request_rec *r) { @@ -66,13 +67,13 @@ int md_http_cert_status(request_rec *r) const char *keyname; apr_bucket_brigade *bb; apr_status_t rv; - + if (!r->parsed_uri.path || strcmp(MD_STATUS_RESOURCE, r->parsed_uri.path)) return DECLINED; - + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "requesting status for: %s", r->hostname); - + /* We are looking for information about a staged certificate */ sc = ap_get_module_config(r->server->module_config, &md_module); if (!sc || !sc->mc || !sc->mc->reg || !sc->mc->certificate_status_enabled) return DECLINED; @@ -84,7 +85,7 @@ int md_http_cert_status(request_rec *r) "md(%s): status supports only GET", md->name); return HTTP_NOT_IMPLEMENTED; } - + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "requesting status for MD: %s", md->name); @@ -94,7 +95,7 @@ int md_http_cert_status(request_rec *r) "loading md status for %s", md->name); return HTTP_INTERNAL_SERVER_ERROR; } - + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "status for MD: %s is %s", md->name, md_json_writep(mdj, r->pool, MD_JSON_FMT_INDENT)); @@ -124,23 +125,23 @@ int md_http_cert_status(request_rec *r) } md_json_setj(cj, resp, keyname, NULL ); } - + if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) { /* copy over the information we want to make public about this: * - when not finished, add an empty object to indicate something is going on * - when a certificate is staged, add the information from that */ cj = md_json_getj(mdj, MD_KEY_RENEWAL, MD_KEY_CERT, NULL); - cj = cj? cj : md_json_create(r->pool);; + cj = cj? cj : md_json_create(r->pool); md_json_setj(cj, resp, MD_KEY_RENEWAL, MD_KEY_CERT, NULL); } - + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "md[%s]: sending status", md->name); - apr_table_set(r->headers_out, "Content-Type", "application/json"); + apr_table_set(r->headers_out, "Content-Type", "application/json"); bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); md_json_writeb(resp, MD_JSON_FMT_INDENT, bb); ap_pass_brigade(r->output_filters, bb); apr_brigade_cleanup(bb); - + return DONE; } @@ -151,10 +152,12 @@ typedef struct { apr_pool_t *p; const md_mod_conf_t *mc; apr_bucket_brigade *bb; + int flags; + const char *prefix; const char *separator; } status_ctx; -typedef struct status_info status_info; +typedef struct status_info status_info; static void add_json_val(status_ctx *ctx, md_json_t *j); @@ -179,13 +182,19 @@ static void si_val_status(status_ctx *ctx, md_json_t *mdj, const status_info *in case MD_S_EXPIRED_DEPRECATED: case MD_S_COMPLETE: until = md_json_get_time(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_UNTIL, NULL); - s = (!until || until > apr_time_now())? "good" : "expired"; + s = (!until || until > apr_time_now())? "good" : "expired"; break; case MD_S_ERROR: s = "error"; break; case MD_S_MISSING_INFORMATION: s = "missing information"; break; default: break; } - apr_brigade_puts(ctx->bb, NULL, NULL, s); + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, s); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%s: %s\n", + ctx->prefix, info->label, s); + } } static void si_val_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) @@ -195,19 +204,28 @@ static void si_val_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) s = url = md_json_gets(mdj, info->key, NULL); if (!url) return; s = md_get_ca_name_from_url(ctx->p, url); - apr_brigade_printf(ctx->bb, NULL, NULL, "%s", - ap_escape_html2(ctx->p, url, 1), - ap_escape_html2(ctx->p, s, 1)); + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s", + ap_escape_html2(ctx->p, url, 1), + ap_escape_html2(ctx->p, s, 1)); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n", + ctx->prefix, info->label, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n", + ctx->prefix, info->label, url); + } } -static void print_date(apr_bucket_brigade *bb, apr_time_t timestamp, const char *title) +static void print_date(status_ctx *ctx, apr_time_t timestamp, const char *title) { + apr_bucket_brigade *bb = ctx->bb; if (timestamp > 0) { char ts[128]; char ts2[128]; apr_time_exp_t texp; apr_size_t len; - + apr_time_exp_gmt(&texp, timestamp); apr_strftime(ts, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); ts[len] = '\0'; @@ -216,14 +234,21 @@ static void print_date(apr_bucket_brigade *bb, apr_time_t timestamp, const char ts2[len] = '\0'; title = ts2; } - apr_brigade_printf(bb, NULL, NULL, - "%s", - ap_escape_html2(bb->p, title, 1), ts); + if (HTML_STATUS(ctx)) { + apr_brigade_printf(bb, NULL, NULL, + "%s", + ap_escape_html2(bb->p, title, 1), ts); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%s%s: %s\n", + ctx->prefix, title, ts); + } } } -static void print_time(apr_bucket_brigade *bb, const char *label, apr_time_t t) +static void print_time(status_ctx *ctx, const char *label, apr_time_t t) { + apr_bucket_brigade *bb = ctx->bb; apr_time_t now; const char *pre, *post, *sep; char ts[APR_RFC822_DATE_LEN]; @@ -231,7 +256,7 @@ static void print_time(apr_bucket_brigade *bb, const char *label, apr_time_t t) apr_time_exp_t texp; apr_size_t len; apr_interval_time_t delta; - + if (t == 0) { /* timestamp is 0, we use that for "not set" */ return; @@ -241,25 +266,32 @@ static void print_time(apr_bucket_brigade *bb, const char *label, apr_time_t t) pre = post = ""; sep = (label && strlen(label))? " " : ""; delta = 0; - apr_rfc822_date(ts, t); - if (t > now) { - delta = t - now; - pre = "in "; - } - else { - delta = now - t; - post = " ago"; - } - if (delta >= (4 * apr_time_from_sec(MD_SECS_PER_DAY))) { - apr_strftime(ts2, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); - ts2[len] = '\0'; - apr_brigade_printf(bb, NULL, NULL, "%s%s%s", - label, sep, ts, ts2); + if (HTML_STATUS(ctx)) { + apr_rfc822_date(ts, t); + if (t > now) { + delta = t - now; + pre = "in "; + } + else { + delta = now - t; + post = " ago"; + } + if (delta >= (4 * apr_time_from_sec(MD_SECS_PER_DAY))) { + apr_strftime(ts2, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); + ts2[len] = '\0'; + apr_brigade_printf(bb, NULL, NULL, "%s%s%s", + label, sep, ts, ts2); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%s%s%s%s%s", + label, sep, ts, pre, md_duration_roughly(bb->p, delta), post); + } } else { - apr_brigade_printf(bb, NULL, NULL, "%s%s%s%s%s", - label, sep, ts, pre, md_duration_roughly(bb->p, delta), post); + delta = t - now; + apr_brigade_printf(bb, NULL, NULL, "%s%s: %" APR_TIME_T_FMT "\n", + ctx->prefix, label, apr_time_sec(delta)); } } @@ -267,37 +299,51 @@ static void si_val_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info { const char *sfrom, *suntil, *sep, *title; apr_time_t from, until; - + sep = NULL; sfrom = md_json_gets(mdj, info->key, MD_KEY_FROM, NULL); from = sfrom? apr_date_parse_rfc(sfrom) : 0; suntil = md_json_gets(mdj, info->key, MD_KEY_UNTIL, NULL); until = suntil?apr_date_parse_rfc(suntil) : 0; - - if (from > apr_time_now()) { - apr_brigade_puts(ctx->bb, NULL, NULL, "from "); - print_date(ctx->bb, from, sfrom); - sep = " "; - } - if (until) { - if (sep) apr_brigade_puts(ctx->bb, NULL, NULL, sep); - apr_brigade_puts(ctx->bb, NULL, NULL, "until "); - title = sfrom? apr_psprintf(ctx->p, "%s - %s", sfrom, suntil) : suntil; - print_date(ctx->bb, until, title); + + if (HTML_STATUS(ctx)) { + if (from > apr_time_now()) { + apr_brigade_puts(ctx->bb, NULL, NULL, "from "); + print_date(ctx, from, sfrom); + sep = " "; + } + if (until) { + if (sep) apr_brigade_puts(ctx->bb, NULL, NULL, sep); + apr_brigade_puts(ctx->bb, NULL, NULL, "until "); + title = sfrom? apr_psprintf(ctx->p, "%s - %s", sfrom, suntil) : suntil; + print_date(ctx, until, title); + } + } + else { + if (from > apr_time_now()) { + print_date(ctx, from, + apr_pstrcat(ctx->p, info->label, "From", NULL)); + } + if (until) { + print_date(ctx, from, + apr_pstrcat(ctx->p, info->label, "Until", NULL)); + } } } static void si_add_header(status_ctx *ctx, const status_info *info) { - const char *html = ap_escape_html2(ctx->p, info->label, 1); - apr_brigade_printf(ctx->bb, NULL, NULL, "%s", html, html); + if (HTML_STATUS(ctx)) { + const char *html = ap_escape_html2(ctx->p, info->label, 1); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s", html, html); + } } static void si_val_cert_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info *info) { md_json_t *jcert; status_info sub = *info; - + sub.key = MD_KEY_VALID; jcert = md_json_getj(mdj, info->key, NULL); if (jcert) si_val_valid_time(ctx, jcert, &sub); @@ -306,11 +352,31 @@ static void si_val_cert_valid_time(status_ctx *ctx, md_json_t *mdj, const status static void si_val_ca_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) { md_json_t *jcert; - status_info sub = *info; - - sub.key = MD_KEY_URL; + jcert = md_json_getj(mdj, info->key, NULL); - if (jcert) si_val_url(ctx, jcert, &sub); + if (jcert) { + const char *proto, *s, *url; + + proto = md_json_gets(jcert, MD_KEY_PROTO, NULL); + s = url = md_json_gets(jcert, MD_KEY_URL, NULL); + if (proto && !strcmp(proto, "tailscale")) { + s = "tailscale"; + } + else if (url) { + s = md_get_ca_name_from_url(ctx->p, url); + } + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s", + ap_escape_html2(ctx->p, url, 1), + ap_escape_html2(ctx->p, s, 1)); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n", + ctx->prefix, info->label, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n", + ctx->prefix, info->label, url); + } + } } static int count_certs(void *baton, const char *key, md_json_t *json) @@ -324,83 +390,139 @@ static int count_certs(void *baton, const char *key, md_json_t *json) return 1; } -static void print_job_summary(apr_bucket_brigade *bb, md_json_t *mdj, const char *key, +static void print_job_summary(status_ctx *ctx, md_json_t *mdj, const char *key, const char *separator) { + apr_bucket_brigade *bb = ctx->bb; char buffer[HUGE_STRING_LEN]; apr_status_t rv; int finished, errors, cert_count; apr_time_t t; const char *s, *line; - + if (!md_json_has_key(mdj, key, NULL)) { return; } - + finished = md_json_getb(mdj, key, MD_KEY_FINISHED, NULL); errors = (int)md_json_getl(mdj, key, MD_KEY_ERRORS, NULL); rv = (apr_status_t)md_json_getl(mdj, key, MD_KEY_LAST, MD_KEY_STATUS, NULL); - + line = separator? separator : ""; if (rv != APR_SUCCESS) { + char *errstr = apr_strerror(rv, buffer, sizeof(buffer)); s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_PROBLEM, NULL); - line = apr_psprintf(bb->p, "%s Error[%s]: %s", line, - apr_strerror(rv, buffer, sizeof(buffer)), s? s : ""); + if (HTML_STATUS(ctx)) { + line = apr_psprintf(bb->p, "%s Error[%s]: %s", line, + errstr, s? s : ""); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sLastStatus: %s\n", ctx->prefix, errstr); + apr_brigade_printf(bb, NULL, NULL, "%sLastProblem: %s\n", ctx->prefix, s); + } + } + + if (!HTML_STATUS(ctx)) { + apr_brigade_printf(bb, NULL, NULL, "%sFinished: %s\n", ctx->prefix, + finished ? "yes" : "no"); } - if (finished) { cert_count = 0; md_json_iterkey(count_certs, &cert_count, mdj, key, MD_KEY_CERT, NULL); - if (cert_count > 0) { - line =apr_psprintf(bb->p, "%s finished, %d new certificate%s staged.", - line, cert_count, cert_count > 1? "s" : ""); + if (HTML_STATUS(ctx)) { + if (cert_count > 0) { + line =apr_psprintf(bb->p, "%s finished, %d new certificate%s staged.", + line, cert_count, cert_count > 1? "s" : ""); + } + else { + line = apr_psprintf(bb->p, "%s finished successfully.", line); + } } else { - line = apr_psprintf(bb->p, "%s finished successfully.", line); + apr_brigade_printf(bb, NULL, NULL, "%sNewStaged: %d\n", ctx->prefix, cert_count); } } else { s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_DETAIL, NULL); - if (s) line = apr_psprintf(bb->p, "%s %s", line, s); + if (s) { + if (HTML_STATUS(ctx)) { + line = apr_psprintf(bb->p, "%s %s", line, s); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sLastDetail: %s\n", ctx->prefix, s); + } + } } - + errors = (int)md_json_getl(mdj, MD_KEY_ERRORS, NULL); if (errors > 0) { - line = apr_psprintf(bb->p, "%s (%d retr%s) ", line, - errors, (errors > 1)? "y" : "ies"); - } - - apr_brigade_puts(bb, NULL, NULL, line); + if (HTML_STATUS(ctx)) { + line = apr_psprintf(bb->p, "%s (%d retr%s) ", line, + errors, (errors > 1)? "y" : "ies"); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sRetries: %d\n", ctx->prefix, errors); + } + } + + if (HTML_STATUS(ctx)) { + apr_brigade_puts(bb, NULL, NULL, line); + } t = md_json_get_time(mdj, key, MD_KEY_NEXT_RUN, NULL); if (t > apr_time_now() && !finished) { - print_time(bb, "\nNext run", t); + print_time(ctx, + HTML_STATUS(ctx) ? "\nNext run" : "NextRun", + t); } - else if (!strlen(line)) { - apr_brigade_puts(bb, NULL, NULL, "\nOngoing..."); + else if (line[0] != '\0') { + if (HTML_STATUS(ctx)) { + apr_brigade_puts(bb, NULL, NULL, "\nOngoing..."); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%s: Ongoing\n", ctx->prefix); + } } } static void si_val_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info) { apr_time_t t; - + const char *prefix = ctx->prefix; + (void)info; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) { - print_job_summary(ctx->bb, mdj, MD_KEY_RENEWAL, NULL); + print_job_summary(ctx, mdj, MD_KEY_RENEWAL, NULL); return; } - + t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); if (t > apr_time_now()) { - print_time(ctx->bb, "Renew", t); + print_time(ctx, "Renew", t); } else if (t) { - apr_brigade_puts(ctx->bb, NULL, NULL, "Pending"); + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "Pending"); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s: %s", ctx->prefix, "Pending"); + } } else if (MD_RENEW_MANUAL == md_json_getl(mdj, MD_KEY_RENEW_MODE, NULL)) { - apr_brigade_puts(ctx->bb, NULL, NULL, "Manual renew"); + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "Manual renew"); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s: %s", ctx->prefix, "Manual renew"); + } + } + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; } } @@ -408,13 +530,33 @@ static int cert_check_iter(void *baton, const char *key, md_json_t *json) { status_ctx *ctx = baton; const char *fingerprint; - + fingerprint = md_json_gets(json, MD_KEY_SHA256_FINGERPRINT, NULL); if (fingerprint) { - apr_brigade_printf(ctx->bb, NULL, NULL, - "%s[%s]
", - ctx->mc->cert_check_url, fingerprint, - ctx->mc->cert_check_name, key); + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, + "%s[%s]
", + ctx->mc->cert_check_url, fingerprint, + ctx->mc->cert_check_name, key); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sType: %s\n", + ctx->prefix, + key); + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sName: %s\n", + ctx->prefix, + ctx->mc->cert_check_name); + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sURL: %s%s\n", + ctx->prefix, + ctx->mc->cert_check_url, fingerprint); + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sFingerprint: %s\n", + ctx->prefix, + fingerprint); + } } return 1; } @@ -423,7 +565,14 @@ static void si_val_remote_check(status_ctx *ctx, md_json_t *mdj, const status_in { (void)info; if (ctx->mc->cert_check_name && ctx->mc->cert_check_url) { + const char *prefix = ctx->prefix; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } md_json_iterkey(cert_check_iter, ctx, mdj, MD_KEY_CERT, NULL); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } } } @@ -431,24 +580,43 @@ static void si_val_stapling(status_ctx *ctx, md_json_t *mdj, const status_info * { (void)info; if (!md_json_getb(mdj, MD_KEY_STAPLING, NULL)) return; - apr_brigade_puts(ctx->bb, NULL, NULL, "on"); + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "on"); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s: on", ctx->prefix); + } } static int json_iter_val(void *data, size_t index, md_json_t *json) { status_ctx *ctx = data; - if (index) apr_brigade_puts(ctx->bb, NULL, NULL, ctx->separator); + const char *prefix = ctx->prefix; + if (HTML_STATUS(ctx)) { + if (index) apr_brigade_puts(ctx->bb, NULL, NULL, ctx->separator); + } + else { + ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL); + } add_json_val(ctx, json); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } return 1; } static void add_json_val(status_ctx *ctx, md_json_t *j) { if (!j) return; - else if (md_json_is(MD_JSON_TYPE_ARRAY, j, NULL)) { + if (md_json_is(MD_JSON_TYPE_ARRAY, j, NULL)) { md_json_itera(json_iter_val, ctx, j, NULL); + return; } - else if (md_json_is(MD_JSON_TYPE_INT, j, NULL)) { + if (!HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, ctx->prefix); + apr_brigade_puts(ctx->bb, NULL, NULL, ": "); + } + if (md_json_is(MD_JSON_TYPE_INT, j, NULL)) { md_json_writeb(j, MD_JSON_FMT_COMPACT, ctx->bb); } else if (md_json_is(MD_JSON_TYPE_STRING, j, NULL)) { @@ -460,13 +628,27 @@ static void add_json_val(status_ctx *ctx, md_json_t *j) else if (md_json_is(MD_JSON_TYPE_BOOL, j, NULL)) { apr_brigade_puts(ctx->bb, NULL, NULL, md_json_getb(j, NULL)? "on" : "off"); } + if (!HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "\n"); + } } static void si_val_names(status_ctx *ctx, md_json_t *mdj, const status_info *info) { - apr_brigade_puts(ctx->bb, NULL, NULL, "
"); + const char *prefix = ctx->prefix; + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "
"); + } + else { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } add_json_val(ctx, md_json_getj(mdj, info->key, NULL)); - apr_brigade_puts(ctx->bb, NULL, NULL, "
"); + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "
"); + } + else { + ctx->prefix = prefix; + } } static void add_status_cell(status_ctx *ctx, md_json_t *mdj, const status_info *info) @@ -475,7 +657,14 @@ static void add_status_cell(status_ctx *ctx, md_json_t *mdj, const status_info * info->fn(ctx, mdj, info); } else { + const char *prefix = ctx->prefix; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } add_json_val(ctx, md_json_getj(mdj, info->key, NULL)); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } } } @@ -486,22 +675,31 @@ static const status_info status_infos[] = { { "Valid", MD_KEY_CERT, si_val_cert_valid_time }, { "CA", MD_KEY_CA, si_val_ca_url }, { "Stapling", MD_KEY_STAPLING, si_val_stapling }, - { "Check@", MD_KEY_SHA256_FINGERPRINT, si_val_remote_check }, - { "Activity", MD_KEY_NOTIFIED, si_val_activity }, + { "CheckAt", MD_KEY_SHA256_FINGERPRINT, si_val_remote_check }, + { "Activity", MD_KEY_NOTIFIED, si_val_activity }, }; static int add_md_row(void *baton, apr_size_t index, md_json_t *mdj) { status_ctx *ctx = baton; + const char *prefix = ctx->prefix; int i; - - apr_brigade_printf(ctx->bb, NULL, NULL, "", (index % 2)? "odd" : "even"); - for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { - apr_brigade_puts(ctx->bb, NULL, NULL, ""); - add_status_cell(ctx, mdj, &status_infos[i]); - apr_brigade_puts(ctx->bb, NULL, NULL, ""); - } - apr_brigade_puts(ctx->bb, NULL, NULL, ""); + + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "", (index % 2)? "odd" : "even"); + for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { + apr_brigade_puts(ctx->bb, NULL, NULL, ""); + add_status_cell(ctx, mdj, &status_infos[i]); + apr_brigade_puts(ctx->bb, NULL, NULL, ""); + } + apr_brigade_puts(ctx->bb, NULL, NULL, ""); + } else { + for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL); + add_status_cell(ctx, mdj, &status_infos[i]); + ctx->prefix = prefix; + } + } return 1; } @@ -514,96 +712,121 @@ int md_domains_status_hook(request_rec *r, int flags) { const md_srv_conf_t *sc; const md_mod_conf_t *mc; - int i, html; + int i; status_ctx ctx; apr_array_header_t *mds; md_json_t *jstatus, *jstock; - + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for managed domains, start"); sc = ap_get_module_config(r->server->module_config, &md_module); if (!sc) return DECLINED; mc = sc->mc; if (!mc || !mc->server_status_enabled) return DECLINED; - html = !(flags & AP_STATUS_SHORT); ctx.p = r->pool; ctx.mc = mc; ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + ctx.flags = flags; + ctx.prefix = "ManagedCertificates"; ctx.separator = " "; mds = apr_array_copy(r->pool, mc->mds); qsort(mds->elts, (size_t)mds->nelts, sizeof(md_t *), md_name_cmp); - if (!html) { - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html summary"); - apr_brigade_puts(ctx.bb, NULL, NULL, "Managed Certificates: "); + if (!HTML_STATUS(&ctx)) { + int total = 0, complete = 0, renewing = 0, errored = 0, ready = 0; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html managed domain status summary"); if (mc->mds->nelts > 0) { md_status_take_stock(&jstock, mds, mc->reg, r->pool); - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON summary"); - apr_brigade_printf(ctx.bb, NULL, NULL, "total=%d, ok=%d renew=%d errored=%d ready=%d", - (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL), - (int)md_json_getl(jstock, MD_KEY_COMPLETE, NULL), - (int)md_json_getl(jstock, MD_KEY_RENEWING, NULL), - (int)md_json_getl(jstock, MD_KEY_ERRORED, NULL), - (int)md_json_getl(jstock, MD_KEY_READY, NULL)); - } - else { - apr_brigade_puts(ctx.bb, NULL, NULL, "[]"); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON managed domain status summary"); + total = (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL); + complete = (int)md_json_getl(jstock, MD_KEY_COMPLETE, NULL); + renewing = (int)md_json_getl(jstock, MD_KEY_RENEWING, NULL); + errored = (int)md_json_getl(jstock, MD_KEY_ERRORED, NULL); + ready = (int)md_json_getl(jstock, MD_KEY_READY, NULL); } - apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sTotal: %d\n", ctx.prefix, total); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sOK: %d\n", ctx.prefix, complete); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sRenew: %d\n", ctx.prefix, renewing); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sErrored: %d\n", ctx.prefix, errored); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sReady: %d\n", ctx.prefix, ready); } - else if (mc->mds->nelts > 0) { - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html table"); + if (mc->mds->nelts > 0) { md_status_get_json(&jstatus, mds, mc->reg, mc->ocsp, r->pool); - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON status"); - apr_brigade_puts(ctx.bb, NULL, NULL, - "
\n

Managed Certificates

\n\n"); - for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { - si_add_header(&ctx, &status_infos[i]); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON managed domain status"); + if (HTML_STATUS(&ctx)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html managed domain status table"); + apr_brigade_puts(ctx.bb, NULL, NULL, + "
\n

Managed Certificates

\n
\n"); + for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { + si_add_header(&ctx, &status_infos[i]); + } + apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + } + else { + ctx.prefix = "ManagedDomain"; } - apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "iterating JSON managed domain status"); md_json_itera(add_md_row, &ctx, jstatus, MD_KEY_MDS, NULL); - apr_brigade_puts(ctx.bb, NULL, NULL, "\n\n
\n"); + if (HTML_STATUS(&ctx)) { + apr_brigade_puts(ctx.bb, NULL, NULL, "\n\n\n"); + } } ap_pass_brigade(r->output_filters, ctx.bb); apr_brigade_cleanup(ctx.bb); ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for managed domains, end"); - + return OK; } static void si_val_ocsp_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info) { apr_time_t t; - + const char *prefix = ctx->prefix; + (void)info; - t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); - print_time(ctx->bb, "Refresh", t); - print_job_summary(ctx->bb, mdj, MD_KEY_RENEWAL, ": "); + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); + print_time(ctx, "Refresh", t); + print_job_summary(ctx, mdj, MD_KEY_RENEWAL, ": "); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } } static const status_info ocsp_status_infos[] = { { "Domain", MD_KEY_DOMAIN, NULL }, - { "Certificate ID", MD_KEY_ID, NULL }, - { "OCSP Status", MD_KEY_STATUS, NULL }, - { "Stapling Valid", MD_KEY_VALID, si_val_valid_time }, + { "CertificateID", MD_KEY_ID, NULL }, + { "OCSPStatus", MD_KEY_STATUS, NULL }, + { "StaplingValid", MD_KEY_VALID, si_val_valid_time }, { "Responder", MD_KEY_URL, si_val_url }, - { "Activity", MD_KEY_NOTIFIED, si_val_ocsp_activity }, + { "Activity", MD_KEY_NOTIFIED, si_val_ocsp_activity }, }; static int add_ocsp_row(void *baton, apr_size_t index, md_json_t *mdj) { status_ctx *ctx = baton; + const char *prefix = ctx->prefix; int i; - - apr_brigade_printf(ctx->bb, NULL, NULL, "", (index % 2)? "odd" : "even"); - for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { - apr_brigade_puts(ctx->bb, NULL, NULL, ""); - add_status_cell(ctx, mdj, &ocsp_status_infos[i]); - apr_brigade_puts(ctx->bb, NULL, NULL, ""); - } - apr_brigade_puts(ctx->bb, NULL, NULL, ""); + + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "", (index % 2)? "odd" : "even"); + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + apr_brigade_puts(ctx->bb, NULL, NULL, ""); + add_status_cell(ctx, mdj, &ocsp_status_infos[i]); + apr_brigade_puts(ctx->bb, NULL, NULL, ""); + } + apr_brigade_puts(ctx->bb, NULL, NULL, ""); + } else { + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL); + add_status_cell(ctx, mdj, &ocsp_status_infos[i]); + ctx->prefix = prefix; + } + } return 1; } @@ -611,53 +834,65 @@ int md_ocsp_status_hook(request_rec *r, int flags) { const md_srv_conf_t *sc; const md_mod_conf_t *mc; - int i, html; + int i; status_ctx ctx; md_json_t *jstatus, *jstock; - + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for ocsp stapling, start"); sc = ap_get_module_config(r->server->module_config, &md_module); if (!sc) return DECLINED; mc = sc->mc; if (!mc || !mc->server_status_enabled) return DECLINED; - html = !(flags & AP_STATUS_SHORT); ctx.p = r->pool; ctx.mc = mc; ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + ctx.flags = flags; + ctx.prefix = "ManagedStaplings"; ctx.separator = " "; - if (!html) { - apr_brigade_puts(ctx.bb, NULL, NULL, "Managed Staplings: "); + if (!HTML_STATUS(&ctx)) { + int total = 0, good = 0, revoked = 0, unknown = 0; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html ocsp stapling status summary"); if (md_ocsp_count(mc->ocsp) > 0) { md_ocsp_get_summary(&jstock, mc->ocsp, r->pool); - apr_brigade_printf(ctx.bb, NULL, NULL, "total=%d, good=%d revoked=%d unknown=%d", - (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL), - (int)md_json_getl(jstock, MD_KEY_GOOD, NULL), - (int)md_json_getl(jstock, MD_KEY_REVOKED, NULL), - (int)md_json_getl(jstock, MD_KEY_UNKNOWN, NULL)); - } - else { - apr_brigade_puts(ctx.bb, NULL, NULL, "[]"); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON ocsp stapling status summary"); + total = (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL); + good = (int)md_json_getl(jstock, MD_KEY_GOOD, NULL); + revoked = (int)md_json_getl(jstock, MD_KEY_REVOKED, NULL); + unknown = (int)md_json_getl(jstock, MD_KEY_UNKNOWN, NULL); } - apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sTotal: %d\n", ctx.prefix, total); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sOK: %d\n", ctx.prefix, good); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sRenew: %d\n", ctx.prefix, revoked); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sErrored: %d\n", ctx.prefix, unknown); } - else if (md_ocsp_count(mc->ocsp) > 0) { + if (md_ocsp_count(mc->ocsp) > 0) { md_ocsp_get_status_all(&jstatus, mc->ocsp, r->pool); - apr_brigade_puts(ctx.bb, NULL, NULL, - "
\n

Managed Staplings

\n\n"); - for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { - si_add_header(&ctx, &ocsp_status_infos[i]); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON ocsp stapling status"); + if (HTML_STATUS(&ctx)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html ocsp stapling status table"); + apr_brigade_puts(ctx.bb, NULL, NULL, + "
\n

Managed Staplings

\n
\n"); + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + si_add_header(&ctx, &ocsp_status_infos[i]); + } + apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + } + else { + ctx.prefix = "ManagedStapling"; } - apr_brigade_puts(ctx.bb, NULL, NULL, "\n"); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "iterating JSON ocsp stapling status"); md_json_itera(add_ocsp_row, &ctx, jstatus, MD_KEY_OCSPS, NULL); - apr_brigade_puts(ctx.bb, NULL, NULL, "\n\n
\n"); + if (HTML_STATUS(&ctx)) { + apr_brigade_puts(ctx.bb, NULL, NULL, "\n\n\n"); + } } ap_pass_brigade(r->output_filters, ctx.bb); apr_brigade_cleanup(ctx.bb); ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for ocsp stapling, end"); - + return OK; } @@ -687,7 +922,7 @@ int md_status_handler(request_rec *r) ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "md-status supports only GET"); return HTTP_NOT_IMPLEMENTED; } - + jstatus = NULL; md = NULL; if (r->path_info && r->path_info[0] == '/' && r->path_info[1] != '\0') { @@ -695,7 +930,7 @@ int md_status_handler(request_rec *r) md = md_get_by_name(mc->mds, name); if (!md) md = md_get_by_domain(mc->mds, name); } - + if (md) { md_status_get_md_json(&jstatus, md, mc->reg, mc->ocsp, r->pool); } @@ -706,12 +941,12 @@ int md_status_handler(request_rec *r) } if (jstatus) { - apr_table_set(r->headers_out, "Content-Type", "application/json"); + apr_table_set(r->headers_out, "Content-Type", "application/json"); bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); md_json_writeb(jstatus, MD_JSON_FMT_INDENT, bb); ap_pass_brigade(r->output_filters, bb); apr_brigade_cleanup(bb); - + return DONE; } return DECLINED; diff --git a/test/modules/md/md_env.py b/test/modules/md/md_env.py index 718e5d1742f..ca07f969374 100755 --- a/test/modules/md/md_env.py +++ b/test/modules/md/md_env.py @@ -111,6 +111,7 @@ class MDTestEnv(HttpdTestEnv): self._a2md_bin = os.path.join(self.bin_dir, 'a2md') self._default_domain = f"test1.{self.http_tld}" + self._tailscale_domain = "test.headless-chicken.ts.net" self._store_dir = "./md" self.set_store_dir_default() @@ -119,6 +120,7 @@ class MDTestEnv(HttpdTestEnv): valid_from=timedelta(days=-100), valid_to=timedelta(days=-10)), CertificateSpec(domains=["localhost"], key_type='rsa2048'), + CertificateSpec(domains=[self._tailscale_domain]), ]) def setup_httpd(self, setup: HttpdTestSetup = None): @@ -168,6 +170,10 @@ class MDTestEnv(HttpdTestEnv): def store_dir(self): return self._store_dir + @property + def tailscale_domain(self): + return self._tailscale_domain + def get_request_domain(self, request): name = request.node.originalname if request.node.originalname else request.node.name return "%s-%s" % (re.sub(r'[_]', '-', name), MDTestEnv.DOMAIN_SUFFIX) diff --git a/test/modules/md/test_780_tailscale.py b/test/modules/md/test_780_tailscale.py new file mode 100644 index 00000000000..84a266b2eb7 --- /dev/null +++ b/test/modules/md/test_780_tailscale.py @@ -0,0 +1,186 @@ +import os +import re +import socket +import sys +from threading import Thread + +import pytest + +from .md_conf import MDConf + + +class TailscaleFaker: + + def __init__(self, env, path): + self.env = env + self._uds_path = path + self._done = False + + def start(self): + def process(self): + self._socket.listen(1) + self._process() + + try: + os.unlink(self._uds_path) + except OSError: + if os.path.exists(self._uds_path): + raise + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.bind(self._uds_path) + self._thread = Thread(target=process, daemon=True, args=[self]) + self._thread.start() + + def stop(self): + self._done = True + self._socket.close() + + def send_error(self, c, status, reason): + c.sendall(f"""HTTP/1.1 {status} {reason}\r +Server: TailscaleFaker\r +Content-Length: 0\r +Connection: close\r +\r +""".encode()) + + def send_data(self, c, ctype: str, data: bytes): + c.sendall(f"""HTTP/1.1 200 OK\r +Server: TailscaleFaker\r +Content-Type: {ctype}\r +Content-Length: {len(data)}\r +Connection: close\r +\r +""".encode() + data) + + def _process(self): + # a http server written on a sunny afternooon + while self._done is False: + try: + c, client_address = self._socket.accept() + try: + data = c.recv(1024) + lines = data.decode().splitlines() + m = re.match(r'^(?P\w+)\s+(?P\S+)\s+HTTP/1.1', lines[0]) + if m is None: + self.send_error(c, 400, "Bad Request") + continue + uri = m.group('uri') + m = re.match(r'/localapi/v0/cert/(?P\S+)\?type=(?P\w+)', uri) + if m is None: + self.send_error(c, 404, "Not Found") + continue + domain = m.group('domain') + cred_type = m.group('type') + creds = self.env.get_credentials_for_name(domain) + sys.stderr.write(f"lookup domain={domain}, type={cred_type} -> {creds}\n") + if creds is None or len(creds) == 0: + self.send_error(c, 404, "Not Found") + continue + if cred_type == 'crt': + self.send_data(c, "text/plain", creds[0].cert_pem) + pass + elif cred_type == 'key': + self.send_data(c, "text/plain", creds[0].pkey_pem) + else: + self.send_error(c, 404, "Not Found") + continue + finally: + c.close() + + except ConnectionAbortedError: + self._done = True + + +class TestTailscale: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env, acme): + UDS_PATH = f"{env.gen_dir}/tailscale.sock" + TestTailscale.UDS_PATH = UDS_PATH + faker = TailscaleFaker(env=env, path=UDS_PATH) + faker.start() + env.APACHE_CONF_SRC = "data/test_auto" + acme.start(config='default') + env.clear_store() + MDConf(env).install() + assert env.apache_restart() == 0 + yield + faker.stop() + + @pytest.fixture(autouse=True, scope='function') + def _method_scope(self, env, request): + env.clear_store() + self.test_domain = env.get_request_domain(request) + + def _write_res_file(self, doc_root, name, content): + if not os.path.exists(doc_root): + os.makedirs(doc_root) + open(os.path.join(doc_root, name), "w").write(content) + + # create a MD using `tailscale` as protocol, wrong path + def test_md_780_001(self, env): + domain = env.tailscale_domain + # generate config with one MD + domains = [domain] + socket_path = '/xxx' + conf = MDConf(env, admin="admin@" + domain) + conf.start_md(domains) + conf.add([ + "MDCertificateProtocol tailscale", + f"MDCertificateAuthority file://{socket_path}", + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + # restart and watch it fail due to wrong tailscale unix socket path + assert env.apache_restart() == 0 + md = env.await_error(domain) + assert md + assert md['renewal']['errors'] > 0 + assert md['renewal']['last']['status-description'] == 'No such file or directory' + assert md['renewal']['last']['detail'] == \ + f"tailscale socket not available, may not be up: {socket_path}" + + # create a MD using `tailscale` as protocol, path to faker, should succeed + def test_md_780_002(self, env): + domain = env.tailscale_domain + # generate config with one MD + domains = [domain] + socket_path = '/xxx' + conf = MDConf(env, admin="admin@" + domain) + conf.start_md(domains) + conf.add([ + "MDCertificateProtocol tailscale", + f"MDCertificateAuthority file://{self.UDS_PATH}", + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + # restart and watch it fail due to wrong tailscale unix socket path + assert env.apache_restart() == 0 + assert env.await_completion(domains) + assert env.apache_restart() == 0 + env.check_md_complete(domain) + + # create a MD using `tailscale` as protocol, but domain name not assigned by tailscale + def test_md_780_003(self, env): + domain = "test.not-correct.ts.net" + # generate config with one MD + domains = [domain] + socket_path = '/xxx' + conf = MDConf(env, admin="admin@" + domain) + conf.start_md(domains) + conf.add([ + "MDCertificateProtocol tailscale", + f"MDCertificateAuthority file://{self.UDS_PATH}", + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + # restart and watch it fail due to wrong tailscale unix socket path + assert env.apache_restart() == 0 + md = env.await_error(domain) + assert md + assert md['renewal']['errors'] > 0 + assert md['renewal']['last']['status-description'] == 'No such file or directory' + assert md['renewal']['last']['detail'] == "retrieving certificate from tailscale" diff --git a/test/modules/md/test_920_status.py b/test/modules/md/test_920_status.py index 36c520ac94b..c89ce6d8d70 100644 --- a/test/modules/md/test_920_status.py +++ b/test/modules/md/test_920_status.py @@ -148,13 +148,17 @@ Protocols h2 http/1.1 acme-tls/1 assert re.search(r'

Managed Certificates

', status, re.MULTILINE) # get the ascii summary status = env.get_server_status(query="?auto", via_domain=env.http_addr, use_https=False) - m = re.search(r'Managed Certificates: total=(\d+), ok=(\d+) renew=(\d+) errored=(\d+) ready=(\d+)', - status, re.MULTILINE) + m = re.search(r'ManagedCertificatesTotal: (\d+)', status, re.MULTILINE) + assert m, status + assert int(m.group(1)) == 1 + m = re.search(r'ManagedCertificatesOK: (\d+)', status, re.MULTILINE) + assert int(m.group(1)) == 0 + m = re.search(r'ManagedCertificatesRenew: (\d+)', status, re.MULTILINE) + assert int(m.group(1)) == 1 + m = re.search(r'ManagedCertificatesErrored: (\d+)', status, re.MULTILINE) + assert int(m.group(1)) == 0 + m = re.search(r'ManagedCertificatesReady: (\d+)', status, re.MULTILINE) assert int(m.group(1)) == 1 - assert int(m.group(2)) == 0 - assert int(m.group(3)) == 1 - assert int(m.group(4)) == 0 - assert int(m.group(5)) == 1 def test_md_920_011(self, env): # MD with static cert files in base server, see issue #161