| /* 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 <assert.h> |
| |
| #include <apr_lib.h> |
| #include <apr_strings.h> |
| |
| #include <httpd.h> |
| #include <http_core.h> |
| #include <http_config.h> |
| #include <http_log.h> |
| #include <http_vhost.h> |
| |
| #include "md.h" |
| #include "md_util.h" |
| #include "mod_md_private.h" |
| #include "mod_md_config.h" |
| |
| |
| #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), |
| 1, |
| 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; |
| conf->transitive = 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)); |
| n->transitive = (add->transitive != DEF_VAL)? add->transitive : base->transitive; |
| 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, "<ManagedDomain")); |
| } |
| |
| static const char *md_section_check(cmd_parms *cmd) { |
| if (!inside_section(cmd)) { |
| return apr_pstrcat(cmd->pool, cmd->cmd->name, |
| " is only valid inside a <ManagedDomain context, not ", |
| cmd->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 "<ManagedDomain > 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 *set_transitive(int *ptransitive, const char *value) |
| { |
| if (!apr_strnatcasecmp("auto", value)) { |
| *ptransitive = 1; |
| return NULL; |
| } |
| else if (!apr_strnatcasecmp("manual", value)) { |
| *ptransitive = 0; |
| return NULL; |
| } |
| return "unknown value, use \"auto|manual\""; |
| } |
| |
| static const char *md_config_sec_add_members(cmd_parms *cmd, void *dc, |
| int argc, char *const argv[]) |
| { |
| md_config_t *config = (md_config_t *)md_config_get(cmd->server); |
| md_config_dir_t *dconfig = dc; |
| apr_array_header_t *domains; |
| const char *err; |
| int i; |
| |
| if (NULL != (err = md_section_check(cmd))) { |
| if (argc == 1) { |
| /* only allowed value outside a section */ |
| return set_transitive(&config->transitive, argv[0]); |
| } |
| return err; |
| } |
| |
| domains = dconfig->md->domains; |
| for (i = 0; i < argc; ++i) { |
| if (NULL != set_transitive(&dconfig->md->transitive, argv[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, transitive = -1; |
| |
| err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE); |
| if (err) { |
| return err; |
| } |
| |
| for (i = 0; i < argc; ++i) { |
| if (NULL != set_transitive(&transitive, argv[i])) { |
| add_domain_name(domains, argv[i], cmd->pool); |
| } |
| } |
| err = md_create(&md, cmd->pool, domains); |
| if (err) { |
| return err; |
| } |
| |
| if (transitive >= 0) { |
| md->transitive = transitive; |
| } |
| |
| if (cmd->config_file) { |
| md->defn_name = cmd->config_file->name; |
| md->defn_line_number = cmd->config_file->line_number; |
| } |
| |
| APR_ARRAY_PUSH(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("<ManagedDomain", md_config_sec_start, NULL, RSRC_CONF, |
| "Container for a manged domain with common settings and certificate."), |
| AP_INIT_TAKE_ARGV("MDMember", md_config_sec_add_members, NULL, RSRC_CONF, |
| "Define domain name(s) part of the Managed Domain. Use 'auto' or " |
| "'manual' to enable/disable auto adding names from virtual hosts."), |
| AP_INIT_TAKE_ARGV("MDMembers", md_config_sec_add_members, NULL, RSRC_CONF, |
| "Define domain name(s) part of the Managed Domain. Use 'auto' or " |
| "'manual' to enable/disable auto adding names from virtual hosts."), |
| AP_INIT_TAKE_ARGV("ManagedDomain", md_config_set_names, NULL, RSRC_CONF, |
| "A group of server names with one certificate"), |
| AP_INIT_TAKE1("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, |
| "URL of CA issueing the certificates"), |
| AP_INIT_TAKE1("MDStoreDir", md_config_set_store_dir, NULL, RSRC_CONF, |
| "the directory for file system storage of managed domain data."), |
| AP_INIT_TAKE1("MDCertificateProtocol", md_config_set_ca_proto, NULL, RSRC_CONF, |
| "Protocol used to obtain/renew certificates"), |
| AP_INIT_TAKE1("MDCertificateAgreement", md_config_set_agreement, NULL, RSRC_CONF, |
| "URL of CA Terms-of-Service agreement you accept"), |
| AP_INIT_TAKE1("MDDriveMode", md_config_set_drive_mode, NULL, RSRC_CONF, |
| "method of obtaining certificates for the managed domain"), |
| AP_INIT_TAKE1("MDRenewWindow", md_config_set_renew_window, NULL, RSRC_CONF, |
| "Time length for renewal before certificate expires (defaults to days)"), |
| AP_INIT_TAKE12("MDPortMap", md_config_set_port_map, NULL, RSRC_CONF, |
| "Declare the mapped ports 80 and 443 on the local server. E.g. 80:8000 " |
| "to indicate that the server port 8000 is reachable as port 80 from the " |
| "internet. Use 80:- to indicate that port 80 is not reachable from " |
| "the outside."), |
| AP_INIT_TAKE_ARGV("MDCAChallenges", md_config_set_cha_tyes, NULL, RSRC_CONF, |
| "A list of challenge types to be used."), |
| AP_END_CMD |
| }; |
| |
| |
| static const md_config_t *config_get_int(server_rec *s, apr_pool_t *p) |
| { |
| md_config_t *cfg = (md_config_t *)ap_get_module_config(s->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; |
| case MD_CONFIG_TRANSITIVE: |
| return (config->transitive != DEF_VAL)? config->transitive : defconf.transitive; |
| 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; |
| } |
| } |