From: Stefan Eissing Date: Tue, 8 Aug 2017 12:08:42 +0000 (+0000) Subject: adding a2md build to modules/md, installed in bin X-Git-Tag: 2.5.0-alpha~234^2~3 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ceeed43c79bae4316297e3ac3a56a533c799487a;p=thirdparty%2Fapache%2Fhttpd.git adding a2md build to modules/md, installed in bin git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/trunk-md@1804405 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/modules/md/Makefile b/modules/md/Makefile index 2390c219c3d..0e0b4f66ffb 100644 --- a/modules/md/Makefile +++ b/modules/md/Makefile @@ -44,8 +44,22 @@ LTLIBRARY_SOURCES = \ LTLIBRARY_DEPENDENCIES = md.h -local-shared-build: $(LTLIBRARY_NAME) $(SHARED_TARGETS) +a2md_OBJECTS = \ + md_cmd_main.c \ + md_cmd_acme.c \ + md_cmd_reg.c \ + md_cmd_store.c + +a2md: $(a2md_OBJECTS) $(LTLIBRARY_NAME) + $(LINK) $(a2md_LTFLAGS) $(a2md_OBJECTS) -lmd $(A2MD_LDADD) $(AP_LIBS) + +# top be installed in bin dir +bin_PROGRAMS = a2md + +TARGETS = $(bin_PROGRAMS) + +local-shared-build: $(LTLIBRARY_NAME) $(SHARED_TARGETS) a2md include $(top_srcdir)/build/library.mk include $(top_srcdir)/build/special.mk diff --git a/modules/md/Makefile.in b/modules/md/Makefile.in index 5f752cab22c..6e4b80e4a01 100644 --- a/modules/md/Makefile.in +++ b/modules/md/Makefile.in @@ -39,8 +39,22 @@ LTLIBRARY_SOURCES = \ LTLIBRARY_DEPENDENCIES = md.h -local-shared-build: $(LTLIBRARY_NAME) $(SHARED_TARGETS) +a2md_OBJECTS = \ + md_cmd_main.c \ + md_cmd_acme.c \ + md_cmd_reg.c \ + md_cmd_store.c + +a2md: $(a2md_OBJECTS) $(LTLIBRARY_NAME) + $(LINK) $(a2md_LTFLAGS) $(a2md_OBJECTS) -lmd $(A2MD_LDADD) $(AP_LIBS) + +# top be installed in bin dir +bin_PROGRAMS = a2md + +TARGETS = $(bin_PROGRAMS) + +local-shared-build: $(LTLIBRARY_NAME) $(SHARED_TARGETS) a2md include $(top_srcdir)/build/library.mk include $(top_srcdir)/build/special.mk diff --git a/modules/md/config2.m4 b/modules/md/config2.m4 index 820d7cb8c43..70edaf843a3 100644 --- a/modules/md/config2.m4 +++ b/modules/md/config2.m4 @@ -270,6 +270,7 @@ APACHE_MODULE(md, [Managed Domain handling], $md_objs, , most, [ fi APR_ADDTO(MOD_MD_LDADD, [ "libmd.la" ]) + APR_ADDTO(A2MD_LDADD, [ "libmd.la" ]) ]) # Ensure that other modules can pick up mod_md.h diff --git a/modules/md/md_cmd.h b/modules/md/md_cmd.h new file mode 100644 index 00000000000..d2778bb4288 --- /dev/null +++ b/modules/md/md_cmd.h @@ -0,0 +1,87 @@ +/* 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_cmd_h +#define md_cmd_h + +struct apr_getopt_option_t; +struct apr_table_t; +struct md_json_t; +struct md_store_t; +struct md_ref_t; +struct md_acme_t; + +typedef struct md_opts md_opts; +typedef struct md_cmd_ctx md_cmd_ctx; +typedef struct md_cmd_t md_cmd_t; + +typedef apr_status_t md_cmd_opt_fn(md_cmd_ctx *ctx, int option, const char *optarg); +typedef apr_status_t md_cmd_do_fn(md_cmd_ctx *ctx, const md_cmd_t *cmd); + +struct md_cmd_ctx { + apr_pool_t *p; + + const char *base_dir; + const char *ca_url; + + struct md_store_t *store; + struct md_reg_t *reg; + struct md_acme_t *acme; + + struct apr_table_t *options; + + const char *tos; + + struct md_json_t *json_out; + + int argc; + const char *const *argv; +}; + +int md_cmd_ctx_has_option(md_cmd_ctx *ctx, const char *key); +const char *md_cmd_ctx_get_option(md_cmd_ctx *ctx, const char *key); + +void md_cmd_ctx_set_option(md_cmd_ctx *ctx, const char *key, const char *value); + + +/* needs */ +#define MD_CTX_NONE 0x0000 +#define MD_CTX_STORE 0x0001 +#define MD_CTX_REG 0x0002 +#define MD_CTX_ACME 0x0004 + +struct md_cmd_t { + const char *name; /* command name */ + int needs; /* command needs: store, reg, acme etc. */ + + md_cmd_opt_fn *opt_fn; /* callback for options handling */ + md_cmd_do_fn *do_fn; /* callback for executing the command */ + + const struct apr_getopt_option_t *opts; /* options definitions */ + const md_cmd_t **sub_cmds; /* sub commands of this command or NULL */ + + const char *synopsis; /* command line synopsis for this command */ + const char *description; /* textual description of this command */ +}; + +extern apr_getopt_option_t MD_NoOptions[]; + +apr_status_t usage(const md_cmd_t *cmd, const char *msg); + +apr_array_header_t *md_cmd_gather_args(md_cmd_ctx *ctx, int index); + +void md_cmd_print_md(md_cmd_ctx *ctx, const md_t *md); + +#endif /* md_cmd_h */ diff --git a/modules/md/md_cmd_acme.c b/modules/md/md_cmd_acme.c new file mode 100644 index 00000000000..650fb4e4695 --- /dev/null +++ b/modules/md/md_cmd_acme.c @@ -0,0 +1,288 @@ +/* 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_acme.h" +#include "md_acme_acct.h" +#include "md_acme_authz.h" +#include "md_json.h" +#include "md_http.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" +#include "md_version.h" +#include "md_cmd.h" +#include "md_cmd_acme.h" + +/**************************************************************************************************/ +/* command: acme newreg */ + +static apr_status_t cmd_acme_newreg(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_status_t rv = APR_SUCCESS; + int i; + + apr_array_header_t *contacts = apr_array_make(ctx->p, 5, sizeof(const char *)); + for (i = 0; i < ctx->argc; ++i) { + APR_ARRAY_PUSH(contacts, const char *) = md_util_schemify(ctx->p, ctx->argv[i], "mailto"); + } + if (apr_is_empty_array(contacts)) { + return usage(cmd, "newreg needs at least one contact email as argument"); + } + + if (APR_SUCCESS == (rv = md_acme_create_acct(ctx->acme, ctx->p, contacts, ctx->tos))) { + md_acme_save(ctx->acme, ctx->store, ctx->p); + fprintf(stdout, "registered: %s\n", md_acme_get_acct(ctx->acme, ctx->p)); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ctx->p, "register new account"); + } + + return rv; +} + +static md_cmd_t AcmeNewregCmd = { + "newreg", MD_CTX_ACME, + NULL, cmd_acme_newreg, MD_NoOptions, NULL, + "newreg contact-uri [contact-uri...]", + "register a new account at ACME server with given contact uri (email)", +}; + +/**************************************************************************************************/ +/* command: acme agree */ + +static apr_status_t acct_agree_tos(md_cmd_ctx *ctx, const char *name, + const char *tos, apr_pool_t *p) +{ + apr_status_t rv; + + if (APR_SUCCESS == (rv = md_acme_use_acct(ctx->acme, ctx->store, ctx->p, name))) { + if (!tos) { + tos = ctx->acme->acct->agreement; + if (!tos) { + return APR_BADARG; + } + } + rv = md_acme_agree(ctx->acme, ctx->p, tos); + if (rv == APR_SUCCESS) { + rv = md_acme_save(ctx->acme, ctx->store, ctx->p); + fprintf(stdout, "agreed terms-of-service: %s\n", ctx->acme->acct->url); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "agree to terms-of-service %s", tos); + } + } + else if (APR_ENOENT == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "unknown account: %s", name); + } + + return rv; +} + +static apr_status_t cmd_acme_agree(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_status_t rv = APR_SUCCESS; + int i; + + for (i = 0; i < ctx->argc; ++i) { + rv = acct_agree_tos(ctx, ctx->argv[i], ctx->tos, ctx->p); + if (rv != APR_SUCCESS) { + break; + } + } + return rv; +} + +static md_cmd_t AcmeAgreeCmd = { + "agree", MD_CTX_STORE|MD_CTX_ACME, + NULL, cmd_acme_agree, MD_NoOptions, NULL, + "agree account", + "agree to ACME terms of service", +}; + +/**************************************************************************************************/ +/* command: acme validate */ + +static apr_status_t acct_validate(md_cmd_ctx *ctx, const char *name, apr_pool_t *p) +{ + apr_status_t rv; + + if (APR_SUCCESS == (rv = md_acme_use_acct(ctx->acme, ctx->store, ctx->p, name))) { + fprintf(stdout, "account valid: %s\n", name); + } + else if (APR_ENOENT == rv) { + fprintf(stderr, "unknown account: %s", name); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "validating account: %s", name); + } + return rv; +} + +static apr_status_t cmd_acme_validate(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_status_t rv = APR_SUCCESS; + int i; + + for (i = 0; i < ctx->argc; ++i) { + rv = acct_validate(ctx, ctx->argv[i], ctx->p); + if (rv != APR_SUCCESS) { + break; + } + } + return rv; +} + +static md_cmd_t AcmeValidateCmd = { + "validate", MD_CTX_STORE|MD_CTX_ACME, + NULL, cmd_acme_validate, MD_NoOptions, NULL, + "validate account", + "validate account existence", +}; + +/**************************************************************************************************/ +/* command: acme delreg */ + +static apr_status_t acme_delreg(md_cmd_ctx *ctx, const char *name, apr_pool_t *p) +{ + apr_status_t rv; + + if (ctx->acme) { + if (APR_SUCCESS == (rv = md_acme_use_acct(ctx->acme, ctx->store, ctx->p, name))) { + rv = md_acme_delete_acct(ctx->acme, ctx->store, ctx->p); + if (rv == APR_SUCCESS) { + fprintf(stdout, "deleted: %s\n", name); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "delete account"); + } + } + else if (APR_ENOENT == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "unknown account: %s", name); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading account: %s", name); + } + } + else if (ctx->store) { + rv = md_acme_unstore_acct(ctx->store, ctx->p, name); + } + else { + rv = APR_EGENERAL; + } + return rv; +} + +static apr_status_t cmd_acme_delreg(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_status_t rv = APR_SUCCESS; + int i; + + for (i = 0; i < ctx->argc; ++i) { + rv = acme_delreg(ctx, ctx->argv[i], ctx->p); + if (rv != APR_SUCCESS) { + break; + } + } + return rv; +} + +static md_cmd_t AcmeDelregCmd = { + "delreg", MD_CTX_STORE, + NULL, cmd_acme_delreg, MD_NoOptions, NULL, + "delreg account", + "delete an existing ACME account", +}; + +/**************************************************************************************************/ +/* command: acme authz */ + +static apr_status_t acme_newauthz(md_cmd_ctx *ctx, md_acme_acct_t *acct, const char *domain) +{ + apr_status_t rv; + md_acme_authz_t *authz; + + rv = md_acme_authz_register(&authz, ctx->acme, ctx->store, domain, ctx->p); + + if (rv == APR_SUCCESS) { + fprintf(stdout, "authz: %s %s\n", domain, authz->location); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ctx->p, "register new authz"); + } + return rv; +} + +static apr_status_t cmd_acme_authz(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + const char *s; + apr_status_t rv; + int i; + + if (ctx->argc <= 0) { + return usage(cmd, NULL); + } + s = ctx->argv[0]; + if (APR_SUCCESS == (rv = md_acme_use_acct(ctx->acme, ctx->store, ctx->p, s))) { + for (i = 1; i < ctx->argc; ++i) { + rv = acme_newauthz(ctx, ctx->acme->acct, ctx->argv[i]); + if (rv != APR_SUCCESS) { + break; + } + } + } + else if (APR_ENOENT == rv) { + fprintf(stderr, "unknown account: %s\n", s); + return APR_EGENERAL; + } + + return rv; +} + +static md_cmd_t AcmeAuthzCmd = { + "authz", MD_CTX_STORE|MD_CTX_ACME, + NULL, cmd_acme_authz, MD_NoOptions, NULL, + "authz account domain", + "request a new authorization for an account and domain", +}; + +/**************************************************************************************************/ +/* command: acme */ + +static const md_cmd_t *AcmeSubCmds[] = { + &AcmeNewregCmd, + &AcmeDelregCmd, + &AcmeAgreeCmd, + &AcmeAuthzCmd, + &AcmeValidateCmd, + NULL +}; + +md_cmd_t MD_AcmeCmd = { + "acme", MD_CTX_STORE, + NULL, NULL, MD_NoOptions, AcmeSubCmds, + "acme cmd [opts] [args]", + "play with the ACME server", +}; diff --git a/modules/md/md_cmd_acme.h b/modules/md/md_cmd_acme.h new file mode 100644 index 00000000000..2aaa05ec7dd --- /dev/null +++ b/modules/md/md_cmd_acme.h @@ -0,0 +1,21 @@ +/* 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_cmd_acme_h +#define md_cmd_acme_h + +extern md_cmd_t MD_AcmeCmd; + +#endif /* md_cmd_acme_h */ diff --git a/modules/md/md_cmd_main.c b/modules/md/md_cmd_main.c new file mode 100644 index 00000000000..5cab34ec834 --- /dev/null +++ b/modules/md/md_cmd_main.c @@ -0,0 +1,437 @@ +/* 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 "md.h" +#include "md_acme.h" +#include "md_json.h" +#include "md_http.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_store_fs.h" +#include "md_util.h" +#include "md_version.h" + +#include "md_cmd.h" +#include "md_cmd_acme.h" +#include "md_cmd_reg.h" +#include "md_cmd_store.h" +#include "md_curl.h" + + +/**************************************************************************************************/ +/* command infrastructure */ + +apr_getopt_option_t MD_NoOptions [] = { + { NULL, 0, 0, NULL } +}; + +apr_status_t usage(const md_cmd_t *cmd, const char *msg) +{ + const apr_getopt_option_t *opt; + int i; + + if (msg) { + fprintf(stderr, "%s\n", msg); + } + fprintf(stderr, "usage: %s\n", cmd->synopsis); + if (cmd->description) { + fprintf(stderr, "\t%s\n", cmd->description); + } + if (cmd->opts[0].name) { + fprintf(stderr, " with the following options:\n"); + + opt = NULL; + for (i = 0; !opt || opt->optch; ++i) { + opt = cmd->opts + i; + if (opt->optch) { + fprintf(stderr, " -%c | --%s %s\t%s\n", + opt->optch, opt->name, opt->has_arg? "arg" : "", opt->description); + + } + } + } + if (cmd->sub_cmds && cmd->sub_cmds[0]) { + fprintf(stderr, " using one of the following commands:\n"); + for (i = 0; cmd->sub_cmds[i]; ++i) { + fprintf(stderr, " \t%s\n", cmd->sub_cmds[i]->synopsis); + fprintf(stderr, " \t\t%s\n", cmd->sub_cmds[i]->description); + } + } + + exit(msg? 1 : 2); +} + +static apr_status_t md_cmd_ctx_init(md_cmd_ctx *ctx, apr_pool_t *p, + int argc, const char *const *argv) +{ + ctx->p = p; + ctx->argc = argc; + ctx->argv = argv; + ctx->options = apr_table_make(p, 5); + + return ctx->options? APR_SUCCESS : APR_ENOMEM; +} + +void md_cmd_ctx_set_option(md_cmd_ctx *ctx, const char *key, const char *value) +{ + apr_table_setn(ctx->options, key, value); +} + +int md_cmd_ctx_has_option(md_cmd_ctx *ctx, const char *option) +{ + return NULL != apr_table_get(ctx->options, option); +} + +const char *md_cmd_ctx_get_option(md_cmd_ctx *ctx, const char *key) +{ + return apr_table_get(ctx->options, key); +} + +static const md_cmd_t *find_cmd(const md_cmd_t **cmds, const char *name) +{ + int i; + if (cmds) { + for (i = 0; cmds[i]; ++i) { + if (!strcmp(name, cmds[i]->name)) { + return cmds[i]; + } + } + } + return NULL; +} + +static apr_status_t cmd_process(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_getopt_t *os; + const char *optarg; + int opt; + apr_status_t rv = APR_SUCCESS; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ctx->p, + "start processing cmd %s", cmd->name); + + apr_getopt_init(&os, ctx->p, ctx->argc, ctx->argv); + while ((rv = apr_getopt_long(os, cmd->opts, &opt, &optarg)) == APR_SUCCESS) { + if (!cmd->opt_fn) { + return usage(cmd, NULL); + } + else if (APR_SUCCESS != (rv = cmd->opt_fn(ctx, opt, optarg))) { + return usage(cmd, NULL); + } + } + if (rv != APR_EOF) { + return usage(cmd, NULL); + } + + if (md_cmd_ctx_has_option(ctx, "help")) { + return usage(cmd, NULL); + } + if (md_cmd_ctx_has_option(ctx, "version")) { + fprintf(stdout, "version: %s\n", MOD_MD_VERSION); + exit(0); + } + + ctx->argv = os->argv + os->ind; + ctx->argc -= os->ind; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ctx->p, "args remaining: %d", ctx->argc); + + if (cmd->needs & (MD_CTX_STORE|MD_CTX_REG|MD_CTX_ACME) && !ctx->store) { + if (!ctx->base_dir) { + fprintf(stderr, "need store directory for command: %s\n", cmd->name); + return APR_EINVAL; + } + if (APR_SUCCESS != (rv = md_store_fs_init(&ctx->store, ctx->p, ctx->base_dir))) { + fprintf(stderr, "error %d creating store for: %s\n", rv, ctx->base_dir); + return APR_EINVAL; + } + } + if (cmd->needs & MD_CTX_REG && !ctx->reg) { + if (!ctx->store) { + fprintf(stderr, "need store for registry: %s\n", cmd->name); + return APR_EINVAL; + } + if (APR_SUCCESS != (rv = md_reg_init(&ctx->reg, ctx->p, ctx->store))) { + fprintf(stderr, "error %d creating registry from store: %s\n", rv, ctx->base_dir); + return APR_EINVAL; + } + } + if (cmd->needs & MD_CTX_ACME && !ctx->acme) { + if (!ctx->store) { + fprintf(stderr, "need store for ACME: %s\n", cmd->name); + return APR_EINVAL; + } + rv = md_acme_create(&ctx->acme, ctx->p, ctx->ca_url); + if (APR_SUCCESS != rv) { + fprintf(stderr, "error creating acme instance %s (%s)\n", + ctx->ca_url, ctx->base_dir); + return rv; + } + rv = md_acme_setup(ctx->acme); + if (rv != APR_SUCCESS) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ctx->p, "contacting %s", ctx->ca_url); + return rv; + } + } + + if (cmd->sub_cmds && cmd->sub_cmds[0]) { + const md_cmd_t *sub_cmd; + + if (!ctx->argc) { + return usage(cmd, "sub command is missing"); + } + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ctx->p, "sub command %s", ctx->argv[0]); + + sub_cmd = find_cmd(cmd->sub_cmds, ctx->argv[0]); + if (sub_cmd) { + return cmd_process(ctx, sub_cmd); + } + else if (!cmd->do_fn) { + fprintf(stderr, "unknown cmd: %s\n", ctx->argv[0]); + return APR_EINVAL; + } + } + + if (cmd->do_fn) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ctx->p, "%s->do_fn", cmd->name); + return cmd->do_fn(ctx, cmd); + } + return APR_EINVAL; +} + +/**************************************************************************************************/ +/* logging setup */ + +static md_log_level_t active_level = MD_LOG_INFO; + +static int log_is_level(void *baton, apr_pool_t *p, md_log_level_t level) +{ + return level <= active_level; +} + +#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]; + char errbuff[32]; + + apr_vsnprintf(buffer, LOG_BUF_LEN-1, fmt, ap); + buffer[LOG_BUF_LEN-1] = '\0'; + + if (rv) { + fprintf(stderr, "[%s:%d %s][%d(%s)] %s\n", file, line, + md_log_level_name(level), rv, + apr_strerror(rv, errbuff, sizeof(errbuff)/sizeof(errbuff[0])), + buffer); + } + else if (active_level == MD_LOG_INFO) { + fprintf(stderr, "%s\n", buffer); + } + else { + fprintf(stderr, "[%s:%d %s][ok] %s\n", file, line, + md_log_level_name(level), buffer); + } + } +} + +/**************************************************************************************************/ +/* utils */ + +void md_cmd_print_md(md_cmd_ctx *ctx, const md_t *md) +{ + assert(md); + if (ctx->json_out) { + md_json_t *json = md_to_json(md, ctx->p); + md_json_addj(json, ctx->json_out, "output", NULL); + } + else { + int i; + fprintf(stdout, "md: %s [", md->name); + for (i = 0; i < md->domains->nelts; ++i) { + const char *domain = APR_ARRAY_IDX(md->domains, i, const char*); + fprintf(stdout, "%s%s", (i? ", " : ""), domain); + } + fprintf(stdout, "]\n"); + } +} + +static int pool_abort(int rv) +{ + abort(); +} + +apr_array_header_t *md_cmd_gather_args(md_cmd_ctx *ctx, int index) +{ + int i; + + apr_array_header_t *args = apr_array_make(ctx->p, 5, sizeof(const char *)); + for (i = index; i < ctx->argc; ++i) { + APR_ARRAY_PUSH(args, const char *) = ctx->argv[i]; + } + return args; +} + +/**************************************************************************************************/ +/* command: main() */ + +static void init_json_out(md_cmd_ctx *ctx) +{ + apr_array_header_t *empty = apr_array_make(ctx->p, 1, sizeof(char*)); + + ctx->json_out = md_json_create(ctx->p); + + md_json_setsa(empty, ctx->json_out, "output", NULL); + md_json_setl(0, ctx->json_out, "status", NULL); +} + +static apr_status_t main_opts(md_cmd_ctx *ctx, int option, const char *optarg) +{ + switch (option) { + case 'a': + ctx->ca_url = optarg; + break; + case 'd': + ctx->base_dir = optarg; + break; + case 'h': + md_cmd_ctx_set_option(ctx, "help", "1"); + break; + case 'j': + init_json_out(ctx); + break; + case 'q': + if (active_level > 0) { + --active_level; + } + break; + case 'v': + if (active_level < MD_LOG_TRACE8) { + ++active_level; + } + break; + case 'V': + md_cmd_ctx_set_option(ctx, "version", "1"); + break; + case 't': + ctx->tos = optarg; + break; + default: + return APR_EINVAL; + } + return APR_SUCCESS; +} + +static const md_cmd_t *MainSubCmds[] = { + &MD_AcmeCmd, + &MD_RegAddCmd, + &MD_RegUpdateCmd, + &MD_RegDriveCmd, + &MD_RegListCmd, + &MD_StoreCmd, + NULL +}; + +static apr_getopt_option_t MainOptions [] = { + { "acme", 'a', 1, "the url of the ACME server directory"}, + { "dir", 'd', 1, "directory for file data"}, + { "help", 'h', 0, "print usage information"}, + { "json", 'j', 0, "produce json output"}, + { "quiet", 'q', 0, "produce less output"}, + { "terms", 't', 1, "you agree to the terms of services (url)" }, + { "verbose", 'v', 0, "produce more output" }, + { "version", 'V', 0, "print version" }, + { NULL, 0, 0, NULL } +}; + +static md_cmd_t MainCmd = { + "a2md", MD_CTX_NONE, + main_opts, NULL, + MainOptions, MainSubCmds, + "a2md [options] cmd [cmd options] [args]", + "Show and manipulate Apache Manged Domains", +}; + +#define BASE_VERSION "apachemd/" MOD_MD_VERSION + +int main(int argc, const char *const *argv) +{ + apr_allocator_t *allocator; + apr_status_t rv; + apr_pool_t *p; + md_cmd_ctx ctx; + + rv = apr_app_initialize(&argc, &argv, NULL); + if (rv != APR_SUCCESS) { + fprintf(stderr, "error initializing APR (error code %d)\n", (int) rv); + return 1; + } + + if (atexit(apr_terminate)) { + perror("error registering atexit"); + return 1; + } + + memset(&ctx, 0, sizeof(ctx)); + md_log_set(log_is_level, log_print, NULL); + + apr_allocator_create(&allocator); + rv = apr_pool_create_ex(&p, NULL, pool_abort, allocator); + if (rv != APR_SUCCESS) { + fprintf(stderr, "error initializing pool\n"); + return 1; + } + + md_http_use_implementation(md_curl_get_impl(p)); + md_acme_init(p, BASE_VERSION); + md_cmd_ctx_init(&ctx, p, argc, argv); + + rv = cmd_process(&ctx, &MainCmd); + + if (ctx.json_out) { + const char *out; + + md_json_setl(rv, ctx.json_out, "status", NULL); + if (APR_SUCCESS != rv) { + char errbuff[32]; + + apr_strerror(rv, errbuff, sizeof(errbuff)/sizeof(errbuff[0])); + md_json_sets(apr_pstrdup(p, errbuff), ctx.json_out, "description", NULL); + } + + out = md_json_writep(ctx.json_out, p, MD_JSON_FMT_INDENT); + if (!out) { + rv = APR_EINVAL; + } + + fprintf(stdout, "%s\n", out ? out : ""); + } + + return (rv == APR_SUCCESS)? 0 : 1; +} diff --git a/modules/md/md_cmd_reg.c b/modules/md/md_cmd_reg.c new file mode 100644 index 00000000000..2fc107d4201 --- /dev/null +++ b/modules/md/md_cmd_reg.c @@ -0,0 +1,367 @@ +/* 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_json.h" +#include "md_http.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" +#include "md_version.h" +#include "md_cmd.h" +#include "md_cmd_reg.h" + +/**************************************************************************************************/ +/* command: add */ + +static apr_status_t cmd_reg_add(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + md_t *md; + const char *err; + apr_status_t rv; + + err = md_create(&md, ctx->p, md_cmd_gather_args(ctx, 0)); + if (err) { + return APR_EINVAL; + } + + md->ca_url = ctx->ca_url; + md->ca_proto = "ACME"; + + rv = md_reg_add(ctx->reg, md, ctx->p); + if (APR_SUCCESS == rv) { + md_cmd_print_md(ctx, md_reg_get(ctx->reg, md->name, ctx->p)); + } + return rv; +} + +md_cmd_t MD_RegAddCmd = { + "add", MD_CTX_REG, + NULL, cmd_reg_add, MD_NoOptions, NULL, + "add [opts] domain [domain...]", + "Adds a new mananged domain. Must not overlap with existing domains.", +}; + +/**************************************************************************************************/ +/* command: list */ + +static int list_add_md(void *baton, md_reg_t *reg, md_t *md) +{ + apr_array_header_t *mdlist = baton; + + APR_ARRAY_PUSH(mdlist, const md_t *) = md; + return 1; +} + +static int md_name_cmp(const void *v1, const void *v2) +{ + return strcmp((*(const md_t**)v1)->name, (*(const md_t**)v2)->name); +} + +static apr_status_t cmd_reg_list(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_array_header_t *mdlist = apr_array_make(ctx->p, 5, sizeof(md_t *)); + const char *name; + const md_t *md; + int i; + + if (ctx->argc > 0) { + for (i = 0; i < ctx->argc; ++i) { + name = ctx->argv[i]; + md = md_reg_get(ctx->reg, name, ctx->p); + if (!md) { + md = md_reg_find(ctx->reg, name, ctx->p); + } + if (!md) { + fprintf(stderr, "managed domain not found: %s\n", name); + return APR_ENOENT; + } + md_cmd_print_md(ctx, md); + } + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ctx->p, "list do"); + md_reg_do(list_add_md, mdlist, ctx->reg, ctx->p); + qsort(mdlist->elts, mdlist->nelts, sizeof(md_t *), md_name_cmp); + + for (i = 0; i < mdlist->nelts; ++i) { + md = APR_ARRAY_IDX(mdlist, i, const md_t*); + md_cmd_print_md(ctx, md); + } + } + + return APR_SUCCESS; +} + +md_cmd_t MD_RegListCmd = { + "list", MD_CTX_REG, + NULL, cmd_reg_list, MD_NoOptions, NULL, + "list", + "list all managed domains" +}; + +/**************************************************************************************************/ +/* command: update */ + +static apr_status_t cmd_reg_update(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + const char *name; + const md_t *md; + md_t *nmd; + apr_status_t rv = APR_SUCCESS; + int i, fields; + + if (ctx->argc <= 0) { + return usage(cmd, "needs md name"); + } + name = ctx->argv[0]; + + md = md_reg_get(ctx->reg, name, ctx->p); + if (NULL == md) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, ctx->p, "%s: not found", name); + return APR_ENOENT; + } + + /* update what */ + fields = 0; + nmd = md_copy(ctx->p, md); + if (NULL == md) { + return APR_ENOMEM; + } + + if (ctx->ca_url && (nmd->ca_url == NULL || strcmp(ctx->ca_url, nmd->ca_url))) { + nmd->ca_url = ctx->ca_url; + fields |= MD_UPD_CA_URL; + } + + if (ctx->argc > 1) { + const char *aspect = ctx->argv[1]; + + if (!strcmp("domains", aspect)) { + nmd->domains = md_cmd_gather_args(ctx, 2); + + if (apr_is_empty_array(nmd->domains)) { + fprintf(stderr, "update domains needs at least 1 domain name as parameter\n"); + return APR_EGENERAL; + } + fields |= MD_UPD_DOMAINS; + } + else if (!strcmp("account", aspect)) { + if (ctx->argc <= 1) { + usage(cmd, "update name account "); + return APR_EINVAL; + } + fields |= MD_UPD_CA_ACCOUNT; + nmd->ca_account = ctx->argv[2]; + } + else if (!strcmp("ca", aspect)) { + if (ctx->argc <= 2) { + usage(cmd, "update name ca [proto]"); + return APR_EINVAL; + } + nmd->ca_url = ctx->argv[2]; + fields |= MD_UPD_CA_URL; + if (ctx->argc > 3) { + nmd->ca_proto = ctx->argv[3]; + fields |= MD_UPD_CA_PROTO; + } + } + else if (!strcmp("contacts", aspect)) { + apr_array_header_t *contacts = apr_array_make(ctx->p, 5, sizeof(const char *)); + for (i = 2; i < ctx->argc; ++i) { + APR_ARRAY_PUSH(contacts, const char *) = + md_util_schemify(ctx->p, ctx->argv[i], "mailto"); + } + nmd->contacts = contacts; + + if (apr_is_empty_array(nmd->contacts)) { + fprintf(stderr, "update contacts needs at least 1 contact email\n"); + return APR_EINVAL; + } + fields |= MD_UPD_CONTACTS; + } + else if (!strcmp("agreement", aspect)) { + if (ctx->argc <= 1) { + usage(cmd, "update name tos "); + return APR_EINVAL; + } + nmd->ca_agreement = ctx->argv[2]; + fields |= MD_UPD_AGREEMENT; + } + else { + fprintf(stderr, "unknown update aspect: %s\n", aspect); + return APR_ENOTIMPL; + } + } + + if (fields) { + if (APR_SUCCESS == (rv = md_reg_update(ctx->reg, ctx->p, md->name, nmd, fields))) { + md = md_reg_get(ctx->reg, md->name, ctx->p); + } + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "no changes necessary"); + } + + if (APR_SUCCESS == rv) { + md_cmd_print_md(ctx, md); + } + return rv; +} + +md_cmd_t MD_RegUpdateCmd = { + "update", MD_CTX_REG, + NULL, cmd_reg_update, MD_NoOptions, NULL, + "update name [ 'aspect' args ]", + "update a managed domain's properties, where 'aspect' is one of: 'domains', 'ca', 'account', " + "'contacts' or 'agreement'" +}; + +/**************************************************************************************************/ +/* command: drive */ + +static apr_status_t assess_and_drive(md_cmd_ctx *ctx, md_t *md) +{ + int errored, force, renew, reset; + const char *challenge, *msg; + apr_status_t rv; + + reset = md_cmd_ctx_has_option(ctx, "reset"); + force = md_cmd_ctx_has_option(ctx, "force"); + challenge = md_cmd_ctx_get_option(ctx, "challenge"); + + if (APR_SUCCESS != (rv = md_reg_assess(ctx->reg, md, &errored, &renew, ctx->p))) { + msg = "error assessing the current state of the " + "Managed Domain. Please check the server " + "logs or run this command in very verbose form and check the output."; + goto out; + } + + if (errored) { + rv = APR_EGENERAL; + msg = "is in error state. Please check the server " + "logs or run this command in very verbose form and check the output."; + goto out; + } + + if (renew || force) { + + msg = "incomplete, sign up"; + if (md->state == MD_S_COMPLETE) { + msg = force? "forcing renewal" : "for renewal"; + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, ctx->p, "%s: %s", md->name, msg); + + if (APR_SUCCESS == (rv = md_reg_stage(ctx->reg, md, challenge, reset, ctx->p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, ctx->p, "%s: loading", md->name); + + rv = md_reg_load(ctx->reg, md->name, ctx->p); + + if (APR_SUCCESS == rv) { + msg = "new credentials active on next server restart"; + } + else { + msg = "error activating new credentials"; + } + } + else { + msg = "error obtaining new credentials"; + } + } + else { + msg = "up-to-date"; + } +out: + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, ctx->p, "%s: %s", md->name, msg); + return rv; +} + +static apr_status_t cmd_reg_drive(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + apr_array_header_t *mdlist = apr_array_make(ctx->p, 5, sizeof(md_t *)); + md_t *md; + apr_status_t rv; + int i; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ctx->p, "drive do"); + if (ctx->argc > 0) { + for (i = 0; i < ctx->argc; ++i) { + md = md_reg_get(ctx->reg, ctx->argv[i], ctx->p); + if (!md) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "%s: not found", ctx->argv[i]); + return APR_ENOENT; + } + APR_ARRAY_PUSH(mdlist, const md_t *) = md; + } + } + else { + md_reg_do(list_add_md, mdlist, ctx->reg, ctx->p); + qsort(mdlist->elts, mdlist->nelts, sizeof(md_t *), md_name_cmp); + } + + rv = APR_SUCCESS; + for (i = 0; i < mdlist->nelts; ++i) { + md_t *md = APR_ARRAY_IDX(mdlist, i, md_t*); + if (APR_SUCCESS != (rv = assess_and_drive(ctx, md))) { + break; + } + } + + return rv; +} + +static apr_status_t cmd_reg_drive_opts(md_cmd_ctx *ctx, int option, const char *optarg) +{ + switch (option) { + case 'c': + md_cmd_ctx_set_option(ctx, "challenge", optarg); + break; + case 'f': + md_cmd_ctx_set_option(ctx, "force", "1"); + break; + case 'r': + md_cmd_ctx_set_option(ctx, "reset", "1"); + break; + default: + return APR_EINVAL; + } + return APR_SUCCESS; +} + +static apr_getopt_option_t DriveOptions [] = { + { "challenge",'c', 1, "which challenge type to use"}, + { "force", 'f', 0, "force driving the managed domain, even when it seems valid"}, + { "reset", 'r', 0, "reset any staging data for the managed domain"}, + { NULL , 0, 0, NULL } +}; + +md_cmd_t MD_RegDriveCmd = { + "drive", MD_CTX_REG, + cmd_reg_drive_opts, cmd_reg_drive, DriveOptions, NULL, + "drive [md...]", + "drive all or the mentioned managed domains toward completeness" +}; + + diff --git a/modules/md/md_cmd_reg.h b/modules/md/md_cmd_reg.h new file mode 100644 index 00000000000..bdb39dc5177 --- /dev/null +++ b/modules/md/md_cmd_reg.h @@ -0,0 +1,24 @@ +/* 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_cmd_reg_h +#define md_cmd_reg_h + +extern md_cmd_t MD_RegAddCmd; +extern md_cmd_t MD_RegUpdateCmd; +extern md_cmd_t MD_RegDriveCmd; +extern md_cmd_t MD_RegListCmd; + +#endif /* md_cmd_reg_h */ diff --git a/modules/md/md_cmd_store.c b/modules/md/md_cmd_store.c new file mode 100644 index 00000000000..41b75a07a7d --- /dev/null +++ b/modules/md/md_cmd_store.c @@ -0,0 +1,229 @@ +/* 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_json.h" +#include "md_http.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" +#include "md_version.h" +#include "md_cmd.h" +#include "md_cmd_store.h" + +/**************************************************************************************************/ +/* command: store add */ + +static apr_status_t cmd_add(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + md_t *md, *nmd; + const char *err; + apr_status_t rv; + + err = md_create(&md, ctx->p, md_cmd_gather_args(ctx, 0)); + if (err) { + return APR_EINVAL; + } + + md->ca_url = ctx->ca_url; + md->ca_proto = "ACME"; + + rv = md_save(ctx->store, ctx->p, MD_SG_DOMAINS, md, 1); + if (APR_SUCCESS == rv) { + md_load(ctx->store, MD_SG_DOMAINS, md->name, &nmd, ctx->p); + md_cmd_print_md(ctx, nmd); + } + return rv; +} + +static md_cmd_t AddCmd = { + "add", MD_CTX_STORE, + NULL, cmd_add, MD_NoOptions, NULL, + "add dns [dns2...]", + "add a new managed domain 'dns' with all the additional domain names", +}; + +/**************************************************************************************************/ +/* command: store remove */ + +static apr_status_t cmd_remove(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + const char *name; + apr_status_t rv; + int i; + + if (ctx->argc <= 0) { + return usage(cmd, "needs at least one name"); + } + + for (i = 0; i < ctx->argc; ++i) { + name = ctx->argv[i]; + rv = md_remove(ctx->store, ctx->p, + MD_SG_DOMAINS, name, md_cmd_ctx_has_option(ctx, "force")); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ctx->p, "removing md %s", name); + break; + } + } + + return rv; +} + +static apr_status_t opts_remove(md_cmd_ctx *ctx, int option, const char *optarg) +{ + switch (option) { + case 'f': + md_cmd_ctx_set_option(ctx, "force", "1"); + break; + default: + return APR_EINVAL; + } + return APR_SUCCESS; +} + +static apr_getopt_option_t RemoveOptions [] = { + { "force", 'f', 0, "force removal, be silent about missing domains"}, + { NULL , 0, 0, NULL } +}; + +static md_cmd_t RemoveCmd = { + "remove", MD_CTX_STORE, + opts_remove, cmd_remove, + RemoveOptions, NULL, + "remove [options] name [name...]", + "remove the managed domains from the store", +}; + +/**************************************************************************************************/ +/* command: store list */ + +static int list_md(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp) +{ + md_cmd_print_md(baton, md); + return 1; +} + +static apr_status_t cmd_list(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + return md_store_md_iter(list_md, ctx, ctx->store, ctx->p, MD_SG_DOMAINS, "*"); +} + +static md_cmd_t ListCmd = { + "list", MD_CTX_STORE, + NULL, cmd_list, MD_NoOptions, NULL, + "list", + "list all managed domains in the store" +}; + +/**************************************************************************************************/ +/* command: store update */ + +static apr_status_t cmd_update(md_cmd_ctx *ctx, const md_cmd_t *cmd) +{ + const char *name; + md_t *md; + apr_status_t rv; + int changed; + + if (ctx->argc <= 0) { + return usage(cmd, "needs md name"); + } + name = ctx->argv[0]; + + rv = md_load(ctx->store, MD_SG_DOMAINS, name, &md, ctx->p); + if (APR_ENOENT == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "%s: not found", name); + return rv; + } + else if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ctx->p, "loading store"); + return rv; + } + + /* update what */ + changed = 0; + + if (ctx->argc > 1) { + const char *aspect = ctx->argv[1]; + + if (!strcmp("domains", aspect)) { + md->domains = md_cmd_gather_args(ctx, 2); + + if (apr_is_empty_array(md->domains)) { + fprintf(stderr, "update domains needs at least 1 domain name as parameter\n"); + return APR_EGENERAL; + } + changed = 1; + } + else { + fprintf(stderr, "unknown update aspect: %s\n", aspect); + return APR_ENOTIMPL; + } + } + + if (ctx->ca_url && (md->ca_url == NULL || strcmp(ctx->ca_url, md->ca_url))) { + md->ca_url = ctx->ca_url; + changed = 1; + } + + if (changed) { + rv = md_save(ctx->store, ctx->p, MD_SG_DOMAINS, md, 0); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "no changes necessary"); + } + + if (APR_SUCCESS == rv) { + rv = md_load(ctx->store, MD_SG_DOMAINS, name, &md, ctx->p); + md_cmd_print_md(ctx, md); + } + return rv; +} + +static md_cmd_t UpdateCmd = { + "update", MD_CTX_STORE, + NULL, cmd_update, MD_NoOptions, NULL, + "update ", + "update the managed domain in the store" +}; + +/**************************************************************************************************/ +/* command: store */ + +static const md_cmd_t *StoreSubCmds[] = { + &AddCmd, + &RemoveCmd, + &ListCmd, + &UpdateCmd, + NULL +}; + +md_cmd_t MD_StoreCmd = { + "store", MD_CTX_STORE, + NULL, NULL, MD_NoOptions, StoreSubCmds, + "store cmd [opts] [args]", + "manipulate the MD store", +}; + diff --git a/modules/md/md_cmd_store.h b/modules/md/md_cmd_store.h new file mode 100644 index 00000000000..b66b6e3c78c --- /dev/null +++ b/modules/md/md_cmd_store.h @@ -0,0 +1,21 @@ +/* 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_cmd_store_h +#define md_cmd_store_h + +extern md_cmd_t MD_StoreCmd; + +#endif /* md_cmd_store_h */