diff --git a/NEWS.adoc b/NEWS.adoc index 4a12f18f63..65cae0d6db 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -106,6 +106,31 @@ https://github.com/networkupstools/nut/milestone/13 allows to pass the `certfile` argument needed for OpenSSL builds. [#3331] * The `libupsclient` (C) and `libnutclient` (C++) API were updated to report the ability to check `CERTIDENT` information. [#3331] + * Introduced support for "authconf" files to store and convey NUT client + authentication details. [issue #3329] + + - `upsc`, `upscmd`, `upsrw` command-line client updates: + * Enabled support for `nutauth.conf` files to provide credentials and/or + SSL settings in the client which previously only did best-effort attempts + at secure communications without an individual certificate, and only + anonymously for reading. The new `-A filename` option defaults to trying + to use a `nutauth.conf` file (if found in one of the default locations) + but not failing if one is not usable; specific values can require use of + such a file (`default`) or to not even try reading one (`none`). + [issues #3329, #3411] + + - `upslog` client/tool updates: + * Added support for best-effort use of `nutauth.conf` files from default + locations or via `-A` option, as described above. Since this client + can establish multiple connections, keep in mind that currently it + can only identify itself with some one (first seen) client certificate, + if `CERTIDENT` settings are used. Multiple `CERTHOST` directives for + specially trusted servers can be used. [#3329] + + - `upsstats`, `upsset`, `upsimage` CGI client updates: + * Added support for best-effort use of `nutauth.conf` files from default + locations described above (no way to choose the location, other than + by web-server environment variables for CGI calls). [#3329] - `upsmon` client updates: * Introduced support for `CERTFILE` option, so the client can identify diff --git a/UPGRADING.adoc b/UPGRADING.adoc index e4beed637a..503986cfec 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -46,6 +46,17 @@ Changes from 2.8.5 to 2.8.6 if the requested value is larger than what is allowed (minus some reserve for configuration files and other use-cases). [issue #3365] +- Enabled support for `nutauth.conf` files to provide credentials and/or + SSL settings in clients which previously only did best-effort attempts at + secure communications without an individual certificate, and only anonymously + for reading like `upsc`. ++ +The new `-A filename` option defaults to trying to use a `nutauth.conf` file + (if found in one of the default locations) but not failing if one is not + usable; specific values can require use of such a file or to not even try + reading one ('none' as the legacy default). See the updated manual pages + for more details. [issues #3329, #3411] + Changes from 2.8.4 to 2.8.5 --------------------------- diff --git a/clients/Makefile.am b/clients/Makefile.am index f26e5a645a..fa2730bccb 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -110,7 +110,7 @@ endif HAVE_CXX11 # Optionally deliverable as part of NUT public API: if WITH_DEV - include_HEADERS = upsclient.h + include_HEADERS = upsclient.h authconf.h if HAVE_CXX11 include_HEADERS += nutclient.h nutclientmem.h else !HAVE_CXX11 @@ -170,7 +170,7 @@ upsstats_cgi_LDADD = $(LDADD_CLIENT) $(top_builddir)/common/libcommonstrjson.la # but it needs nut_version.h made before the rest of build, # to include it into upsclient.c (without an explicit link, # this target is sometimes missed in parallel builds): -libupsclient_la_SOURCES = upsclient.c upsclient.h +libupsclient_la_SOURCES = upsclient.c upsclient.h authconf.c authconf.h # See comments for similar trick in common/Makefile.am for common-nut_version.c if BUILDING_IN_TREE diff --git a/clients/authconf.c b/clients/authconf.c new file mode 100644 index 0000000000..aff088a44d --- /dev/null +++ b/clients/authconf.c @@ -0,0 +1,1254 @@ +/* authconf.c - handling NUT client authentication configuration parsing + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" +#include "common.h" + +#include "authconf.h" +#include "parseconf.h" +#include "upsclient.h" + +#include +#include +#include +#include +#include + +#ifndef WIN32 +# include +# include +# include +# include +#else /* => WIN32 */ +/* Those 2 files for support of getaddrinfo, getnameinfo and freeaddrinfo + on Windows 2000 and older versions */ +# include +# include +/* This override network system calls to adapt to Windows specificity */ +# define W32_NETWORK_CALL_OVERRIDE +# include "wincompat.h" +# undef W32_NETWORK_CALL_OVERRIDE +#endif /* WIN32 */ + +#include "strcasestr-static.h" + +static upscli_authconf_t *authconf_list = NULL; +/** Shortcut: link to the section in authconf_list whose lines we are currently + * editing in the configuration reader; if NULL, we are editing global defaults */ +static upscli_authconf_t *current_section = NULL; +/** Shortcut: link to the (probably first) section in authconf_list with null + * "section" name */ +static upscli_authconf_t *global_defaults = NULL; +/** Does the section title of current_section include a non-trivial "user" + * name component (would we ignore a USER directive, if present)? */ +static int current_section_with_fixed_username = 0; +/** Is the section title of current_section ignored in the configuration reader + * (e.g. ignored because of a section-scope directive and does not match the + * name of current_section after normalization, or a reserved title for the + * global section while not in its context)? */ +static int current_section_ignored = 0; + +static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope); + +upscli_authconf_t *upscli_get_authconf_list(void) +{ + return authconf_list; +} + +upscli_authconf_t *upscli_create_authconf_item(const char *section) +{ + upscli_authconf_t *node = (upscli_authconf_t *)calloc(1, sizeof(upscli_authconf_t)); + + if (!node) { + upsdebugx(1, "Failed to create nutauth configuration node for section '%s'", NUT_STRARG(section)); + return NULL; + } + + if (section) { + /* FIXME: normalize section */ + node->section = xstrdup(section); + } + node->certverify = -1; + node->forcessl = -1; + + return node; +} + +upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const char *section) +{ + upscli_authconf_t *node = upscli_create_authconf_item(section); + + if (!node) { + upsdebugx(1, "Failed to create nutauth configuration node for section '%s'", NUT_STRARG(section)); + return NULL; + } + + if (source) { + const char *at = NULL; + + if (!section) + node->section = source->section ? xstrdup(source->section) : NULL; + + if ( ((at = strchr(node->section, '@')) != NULL) + && at != node->section + ) { + /* New section title strictly defines a user name */ + node->user = (char*)xcalloc(at - node->section + 1, sizeof(char)); + memcpy(node->user, node->section, at - node->section); + } else { + /* No '@' or no username chars before it */ + node->user = source->user ? xstrdup(source->user) : NULL; + } + + node->pass = source->pass ? xstrdup(source->pass) : NULL; + node->certpath = source->certpath ? xstrdup(source->certpath) : NULL; + node->certfile = source->certfile ? xstrdup(source->certfile) : NULL; + node->certident = source->certident ? xstrdup(source->certident) : NULL; + node->certpasswd = source->certpasswd ? xstrdup(source->certpasswd) : NULL; + node->ssl_backend = source->ssl_backend ? xstrdup(source->ssl_backend) : NULL; + + node->certhost = source->certhost ? xstrdup(source->certhost) : NULL; + node->certverify = source->certverify; + node->forcessl = source->forcessl; + } + + return node; +} + +/** Merge contents of two existing configuration items, they may be or not be on the list */ +upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_authconf_t *target) +{ + const char *at = NULL; + + if (!source) + return target; + + /* TOTHINK: (re-)normalize? */ + if ( (!(target->section) || !*(target->section)) + && (source->section && *(source->section)) + ) { + free(target->section); + target->section = xstrdup(source->section); + } + + if ( ((at = strchr(target->section, '@')) != NULL) + && at != target->section + ) { + /* Target section title strictly defines a user name */ + free(target->user); + target->user = (char*)xcalloc(at - target->section + 1, sizeof(char)); + memcpy(target->user, target->section, at - target->section); + } else { + /* No '@' or no username chars before it in target section title */ + if (!(target->user) && source->user) { + target->user = xstrdup(source->user); + } /* else keep what was there */ + } + + /* Replace only NULL strings; keep existing ones even if empty */ + if (!(target->pass) && source->pass) { + target->pass = xstrdup(source->pass); + } + + if (!(target->certpath) && source->certpath) { + target->certpath = xstrdup(source->certpath); + } + + if (!(target->certfile) && source->certfile) { + target->certfile = xstrdup(source->certfile); + } + + if (!(target->certident) && source->certident) { + target->certident = xstrdup(source->certident); + } + + if (!(target->certpasswd) && source->certpasswd) { + target->certpasswd = xstrdup(source->certpasswd); + } + + if (!(target->ssl_backend) && source->ssl_backend) { + target->ssl_backend = xstrdup(source->ssl_backend); + } + + if (!(target->certhost) && source->certhost) { + target->certhost = xstrdup(source->certhost); + } + + if (target->certverify < 0 && source->certverify >= 0) { + target->certverify = source->certverify; + } + + if (target->forcessl < 0 && source->forcessl >= 0) { + target->forcessl = source->forcessl; + } + + return target; +} + +static upscli_authconf_t *upscli_add_authconf(upscli_authconf_t* node) +{ + if (!node) + return NULL; + + /* Append to end of list */ + if (!authconf_list) { + authconf_list = node; + } else { + upscli_authconf_t *tmp = authconf_list; + while (tmp->next) { + tmp = tmp->next; + } + tmp->next = node; + } + + return node; +} + +static upscli_authconf_t *upscli_add_authconf_item(const char *section) +{ + upscli_authconf_t *node = upscli_create_authconf_item(section); + + if (!node) { + fatalx(EXIT_FAILURE, "Failed to create nutauth configuration node for section '%s' which should be added to the list", NUT_STRARG(section)); + } + + return upscli_add_authconf(node); +} + +upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node) +{ + if (node) { + upscli_authconf_t *next = node->next; + + free(node->section); + free(node->user); + free(node->pass); + free(node->certpath); + free(node->certfile); + free(node->certident); + free(node->certpasswd); + free(node->ssl_backend); + free(node->certhost); + + free(node); + + return next; + } + + return NULL; +} + +static int upscli_dump_authconf_line_str(FILE *stream, const char *var, const char *val, const char *indent, int for_debug) +{ + /* Assume sane inputs from upscli_dump_authconf_item(); val may be NULL */ + int res = 0; + if (!val) { + if (for_debug) { + res = fprintf(stream, + "%s%s = \n", + indent, var + ); + } + return 0; + } else { + if (for_debug == 1 && *val) { + char enc[LARGEBUF]; + res = fprintf(stream, + "%s%s = \"%s\"\n", + indent, var, pconf_encode(val, enc, sizeof(enc)) + ); + } else { + res = fprintf(stream, + "%s%s = \"%s\"\n", + indent, var, val + ); + } + } + + if (res < 0) { + upsdebugx(5, "%s: failed (%d) to effectively print %s='%s'", __func__, res, NUT_STRARG(var), NUT_STRARG(val)); + } + return res; +} + +static int upscli_dump_authconf_line_int(FILE *stream, const char *var, int val, const char *indent, int for_debug) +{ + /* Assume sane inputs from upscli_dump_authconf_item(); val may be NULL */ + int res; + + /* TOTHINK: Print "-1" values when not running "for_debug"? + * We do parse them to hop over to a better preference... */ + NUT_UNUSED_VARIABLE(for_debug); + + res = fprintf(stream, + "%s%s = %d\n", + indent, var, (int)val + ); + + if (res < 0) { + upsdebugx(5, "%s: failed (%d) to effectively print %s=%d", __func__, res, NUT_STRARG(var), val); + } + return res; +} + +int upscli_dump_authconf_item(FILE *stream, upscli_authconf_t *node, int for_debug, int show_pass) +{ + char *indent = NULL; + int res = 0, ret = 0; + + if (!node) + return -1; + + if (!stream) + stream = stdout; + + if (node->section && *(node->section)) { + indent = "\t"; + res = fprintf(stream, "[%s]\n", node->section); + } else { + /* Global section */ + if (for_debug) { + indent = "\t"; + res = fprintf(stream, "[]\n"); + } else { + indent = ""; + res = 0; + } + } + + if (res < 0) + return res; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "USER", node->user, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "PASS", show_pass || !(node->pass) ? node->pass : "", indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "CERTPATH", node->certpath, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "CERTFILE", node->certfile, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "CERTIDENT_NAME", node->certident, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "CERTIDENT_PASS", show_pass || !(node->certpasswd) ? node->certpasswd : "", indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "SSLBACKEND", node->ssl_backend, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_str(stream, "CERTHOST", node->certhost, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_int(stream, "CERTVERIFY", node->certverify, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_int(stream, "FORCESSL", node->forcessl, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + return ret; +} + +size_t upscli_dump_authconf_list(FILE *stream, int for_debug, int show_pass) +{ + upscli_authconf_t *node = authconf_list; + size_t count = 0; + + while (node) { + count++; + upscli_dump_authconf_item(stream, node, for_debug, show_pass); + node = node->next; + } + + return count; +} + +void upscli_free_authconf_list(void) +{ + upscli_authconf_t *node = authconf_list; + + while (node) { + node = upscli_free_authconf_item(node); + } + + authconf_list = NULL; + current_section = NULL; + global_defaults = NULL; +} + +static void set_authconf_val(upscli_authconf_t *conf, const char *var, const char *val) +{ + if (!conf || !var) + return; + + if (!strcasecmp(var, "user") || !strcasecmp(var, "username")) { + if (current_section_with_fixed_username && conf->user + && (!val || (val && strcmp(conf->user, val))) + ) { + upslogx(LOG_WARNING, "USER keyword ignored for a section named like 'user@host:port'"); + return; + } + free(conf->user); + conf->user = val ? xstrdup(val) : NULL; + } else if (!strcasecmp(var, "pass") || !strcasecmp(var, "password")) { + free(conf->pass); + conf->pass = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTPATH")) { + free(conf->certpath); + conf->certpath = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTFILE")) { + free(conf->certfile); + conf->certfile = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTIDENT_NAME")) { + free(conf->certident); + conf->certident = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTIDENT_PASS")) { + free(conf->certpasswd); + conf->certpasswd = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "SSLBACKEND")) { + free(conf->ssl_backend); + conf->ssl_backend = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTHOST")) { + free(conf->certhost); + conf->certhost = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTVERIFY")) { + if (val) { + if (!strcasecmp(val, "on") || !strcasecmp(val, "yes") || !strcmp(val, "1")) + conf->certverify = 1; + else if (!strcasecmp(val, "off") || !strcasecmp(val, "no") || !strcmp(val, "0")) + conf->certverify = 0; + } + } else if (!strcmp(var, "FORCESSL")) { + if (val) { + if (!strcasecmp(val, "on") || !strcasecmp(val, "yes") || !strcmp(val, "1")) + conf->forcessl = 1; + else if (!strcasecmp(val, "off") || !strcasecmp(val, "no") || !strcmp(val, "0")) + conf->forcessl = 0; + } + } else { + upslogx(LOG_WARNING, "Unrecognized authconf keyword: '%s'", var); + } +} + +static void authconf_err(const char *errmsg) +{ + upslogx(LOG_ERR, "Error in parseconf(authconf): %s", errmsg); +} + +int upscli_normalize_authconf_section_parts( + char **out_normalized_sect_name, + char **p_sect_user, + int *out_fixed_sect_user, + char **p_sect_host, + char **p_sect_port) +{ + char *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; + + /* All p_* args must be non-NULL pointers to `char *` string variables + * which may be freed and re-allocated to return normalized values + * (original strings may themselves be NULL). + * The out_* values are optional and may be NULL if you do not want + * those data points returned. + */ + if (!p_sect_user || !p_sect_host || !p_sect_port) { + upslogx(LOG_ERR, "upscli_normalize_authconf_section_parts: NULL pointer-to-string argument provided"); + return -1; + } + + /* No changes imposed here */ + sect_user = *p_sect_user; + + sect_host = *p_sect_host; + if (!sect_host || !*sect_host) { + sect_host = xstrdup("localhost"); + if (!sect_host) goto failed; + } + + sect_port = *p_sect_port; + if (sect_port && *sect_port) { + /* As port is a string, resolve it (if not a number, + * try to get one via "services" naming database) */ + char *p = sect_port; + int is_numeric = 1; + + while (*p) { + if (!isdigit((unsigned char)*p)) { + is_numeric = 0; + break; + } + p++; + } + + if (!is_numeric) { + struct servent *se = getservbyname(sect_port, "tcp"); + + if (se) { + char portbuf[16]; + if (snprintf(portbuf, sizeof(portbuf), "%u", (unsigned int)ntohs(se->s_port)) < 1) { + upsdebugx(1, "%s: Failed to construct port number from service name", __func__); + goto failed; + } + sect_port = xstrdup(portbuf); + if (!sect_port) goto failed; + } else { + upslogx(LOG_WARNING, "%s: Failed to resolve port number from service name '%s', " + "keeping original string but it is likely useless", __func__, sect_port); + } + } + } else { + char portbuf[16]; + if (snprintf(portbuf, sizeof(portbuf), "%u", (unsigned int)NUT_PORT) < 1) { + upsdebugx(1, "%s: Failed to construct default port number", __func__); + goto failed; + } + sect_port = xstrdup(portbuf); + if (!sect_port) goto failed; + } + + /* Only now that we (almost) do not expect failures, we can + * consistently populate caller's output variables (if any) */ + if (out_normalized_sect_name) { + char normalized_sect_name_buf[LARGEBUF]; + + if (snprintf(normalized_sect_name_buf, sizeof(normalized_sect_name_buf), "%s@%s:%s", + sect_user ? sect_user : "", + sect_host, + sect_port) > 0 + ) { + free(*out_normalized_sect_name); + *out_normalized_sect_name = xstrdup(normalized_sect_name_buf); + } else { + upsdebugx(1, "%s: Failed to reconstruct normalized section header", __func__); + goto failed; + } + } + + if (out_fixed_sect_user) + *out_fixed_sect_user = (sect_user && *sect_user); + + /* Different pointers? */ + if (*p_sect_host != sect_host) { + free(*p_sect_host); + *p_sect_host = sect_host; + } + + if (*p_sect_port != sect_port) { + free(*p_sect_port); + *p_sect_port = sect_port; + } + + return 0; + +failed: + free(sect_user); + free(sect_host); + free(sect_port); + + return -1; +} + +int upscli_split_authconf_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port) +{ + /* Take raw sect_name as input (e.g. a user-written string from config files). + * Normalize it by splitting into user, host, and port components (populating absent values). + * Return normalized components and reconstructed section name in output parameters (if not NULL). + */ + const char *at = NULL, *colon = NULL; + char *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; + int fixed_sect_user = 0; + + if (!sect_name) { + upsdebugx(1, "%s: sect_name is NULL", __func__); + return -1; + } + + if (!(*sect_name)) { + /* TOTHINK: Should this mean `localhost@NUT_PORT`? Or global? Probably neither. */ + upsdebugx(1, "%s: sect_name is empty", __func__); + return -1; + } + + at = strchr(sect_name, '@'); + colon = strchr(sect_name, ':'); + if (at && colon && colon < at) { + upsdebugx(1, "%s: Invalid section header: colon ':' before at '@': '%s'", __func__, sect_name); + return -1; + } + + fixed_sect_user = (at && at != sect_name); + if (fixed_sect_user) { + /* If section matched user@host:port, ensure user is set to this user */ + sect_user = xstrdup(sect_name); + if (!sect_user) goto failed; + sect_user[at - sect_name] = '\0'; + } /* else keep sect_user=NULL */ + + if (at) { + if (at + 1 != colon) { + sect_host = xstrdup(at + 1); + if (!sect_host) goto failed; + if (colon) { + sect_host[colon - at - 1] = '\0'; + } + } /* else keep sect_host=NULL */ + } else { + /* No "user@" part, so just use sect_name as host - + * just because this is more likely than this being + * a specified sect_user at implicit "localhost" */ + if (sect_name + 1 != colon) { + sect_host = xstrdup(sect_name); + if (!sect_host) goto failed; + if (colon) { + sect_host[colon - sect_name] = '\0'; + } + } /* else keep sect_host=NULL */ + } + + if (colon && colon[1]) { + /* May get re-normalized below */ + sect_port = xstrdup(colon + 1); + if (!sect_port) goto failed; + } + + if (upscli_normalize_authconf_section_parts( + normalized_sect_name, + §_user, &fixed_sect_user, + §_host, §_port) < 0 + ) goto failed; + + if (out_fixed_sect_user) + *out_fixed_sect_user = fixed_sect_user; + + if (normalized_sect_user) { + *normalized_sect_user = sect_user; + } else { + free(sect_user); + } + + if (normalized_sect_host) { + *normalized_sect_host = sect_host; + } else { + free(sect_host); + } + + if (normalized_sect_port) { + *normalized_sect_port = sect_port; + } else { + free(sect_port); + } + + return 0; + +failed: + free(sect_user); + free(sect_host); + free(sect_port); + + return -1; +} + +static void handle_authconf_args(size_t numargs, char **arg, int global_scope) +{ + /* Property: var = val */ + const char *var = NULL, *val = NULL; + + if (numargs < 1) + return; + + /* Section header [section] */ + if (arg[0][0] == '[' && arg[0][strlen(arg[0])-1] == ']') { + char *sect_name = NULL, *sect_user = NULL, *sect_host = NULL, *sect_port = NULL, *normalized_sect_name = NULL; + const char *end_bracket = NULL; + upscli_authconf_t *tmp = NULL; + + if (current_section) { + upsdebugx(3, "%s: finished handling section %s", __func__, NUT_STRARG(current_section->section)); + if (current_section->section + && current_section->certhost + && *(current_section->certhost) + && upscli_split_authconf_section( + current_section->section, + &normalized_sect_name, + §_user, + ¤t_section_with_fixed_username, + §_host, §_port) >= 0 + && sect_host && *sect_host + && sect_port && *sect_port + ) { + upscli_add_host_port_cert( + sect_host, + (uint16_t)atol(sect_port), + current_section->certhost, + current_section->certverify, + current_section->forcessl); + } + } + + current_section_ignored = 0; + + sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ + end_bracket = strchr(sect_name, ']'); + if (!end_bracket || !strcmp(sect_name, "_global_defaults")) { + free(sect_name); + + if (global_scope) { + /* Subsequent lines will (re-)populate global_defaults */ + current_section = NULL; + return; + } + + current_section_ignored = 1; + upslogx(LOG_WARNING, "%s: Invalid nutauth section header format " + "in a non-global context, section contents will be ignored: %s", + __func__, arg[0]); + return; + } + + *(char *)(end_bracket) = '\0'; /* forget trailing ']' and any characters after it (comments etc.) */ + + if (upscli_split_authconf_section(sect_name, &normalized_sect_name, + §_user, ¤t_section_with_fixed_username, + §_host, §_port) < 0 + ) { + free(normalized_sect_name); + free(sect_name); + free(sect_user); + free(sect_host); + free(sect_port); + fatalx(EXIT_FAILURE, "Invalid nutauth section header: %s", NUT_STRARG(arg[0])); + } + + if (!global_scope && current_section + && (!current_section->section || strcmp(current_section->section, normalized_sect_name)) + ) { + upslogx(LOG_WARNING, "Section header [%s] ignored in included file with " + "section-scope for [%s], section contents will be ignored", + normalized_sect_name, current_section->section); + current_section_ignored = 1; + return; + } + + /* Find if section already exists */ + upsdebugx(4, "%s: Checking for existing section [%s] to import [%s]", + __func__, normalized_sect_name, sect_name); + current_section = NULL; + tmp = authconf_list; + while (tmp) { + if (tmp->section && !strcmp(tmp->section, normalized_sect_name)) { + current_section = tmp; + break; + } + tmp = tmp->next; + } + + if (!current_section) { + current_section = upscli_add_authconf_item(normalized_sect_name); + + if (current_section_with_fixed_username && sect_user && *sect_user) { + /* If section matched user@host:port, ensure + * that user field is set to this non-trivial + * value and is not modified later. */ + current_section->user = xstrdup(sect_user); + } + + /* Subsequent calls will parse lines to populate fields + * in this new section, if any; keep NULL's otherwise. + * To copy global defaults (or host defaults into an + * exact-match) to fill in the missing points, see + * upscli_get_authconf_item() for an effective complete + * momentary final configuration needed for a connection. + */ + } + + free(normalized_sect_name); + free(sect_name); + free(sect_user); + free(sect_host); + free(sect_port); + return; + } + + if (current_section_ignored) { + return; + } + + /* INCLUDE support */ + if (!strcasecmp(arg[0], "INCLUDE_REQUIRED")) { + if (numargs < 2) { + fatalx(EXIT_FAILURE, "INCLUDE_REQUIRED missing filename"); + } + + /* If we are in global scope (current_section == NULL), sub-includes are global scope. + * If we are in a section, sub-includes are section scope. + */ + parse_authconf_file(arg[1], 1, (current_section == NULL)); + return; + } + + if (!strcasecmp(arg[0], "INCLUDE")) { + if (numargs < 2) { + upslogx(LOG_ERR, "INCLUDE missing filename"); + return; + } + + /* If we are in global scope (current_section == NULL), sub-includes are global scope. + * If we are in a section, sub-includes are section scope. + */ + parse_authconf_file(arg[1], 0, (current_section == NULL)); + return; + } + + /* While above we technically also handled possible arg[0] values, + * they were not variable names - and so were not called that. */ + var = arg[0]; + if (numargs >= 3 && !strcmp(arg[1], "=")) { + upsdebugx(6, "%s: handling line with directive '%s' = '%s'", + __func__, NUT_STRARG(arg[0]), + strcasestr(arg[0], "pass") ? "" : NUT_STRARG(arg[2])); + val = arg[2]; + } else if (numargs == 1) { + /* Flag property? */ + upsdebugx(5, "%s: line with only directive '%s' did not contain '= ...', " + "assuming this is a numeric flag set to \"1\".", + __func__, NUT_STRARG(arg[0])); + val = "1"; + } else { + upslogx(LOG_WARNING, "Malformed line starting with directive '%s' and %" + PRIuSIZE " tokens overall, assuming NULL value assignment", + NUT_STRARG(arg[0]), numargs); + } + + if (current_section) { + set_authconf_val(current_section, var, val); + } else { + /* Creating/modifying global defaults */ + if (!global_defaults) { + global_defaults = upscli_add_authconf_item(NULL); + } + + /* Initial spec says global-scope includes may modify + * global default items, as well as define new sections + * or overlay items in existing sections. + * This implementation handles this by remembering the + * most-recent "current_section" state. + */ + set_authconf_val(global_defaults, var, val); + } +} + +static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope) +{ + PCONF_CTX_t ctx; + + check_perms(filename); + + if (!pconf_init(&ctx, authconf_err)) { + if (fatal_errors) { + exit(EXIT_FAILURE); + } + return -1; + } + + if (!pconf_file_begin(&ctx, filename)) { + if (fatal_errors) { + fatalx(EXIT_FAILURE, "Can't open %s: %s", filename, ctx.errmsg); + } else { + upslogx(LOG_WARNING, "Can't open %s: %s", filename, ctx.errmsg); + pconf_finish(&ctx); + return -1; + } + } + + while (pconf_file_next(&ctx)) { + if (pconf_parse_error(&ctx)) { + upslogx(LOG_ERR, "Parse error: %s:%d: %s", filename, ctx.linenum, ctx.errmsg); + continue; + } + handle_authconf_args(ctx.numargs, ctx.arglist, global_scope); + } + + /* A next included file may have a different section scope, even if it has no title. + * TOTHINK: We should not reset the current_section pointer to NULL here, right? */ + current_section_ignored = 0; + + pconf_finish(&ctx); + return 1; +} + +int upscli_read_authconf_file(const char *filename, int fatal_errors) +{ + char fn[NUT_PATH_MAX + 1]; + + /* Ensure we start fresh if called multiple times */ + upscli_free_authconf_list(); + + if (!filename) { + /* Select a starting point - whichever default expected file exists; + * it may INCLUDE further files as wanted by user or site sysadmin. + */ + struct stat st; + char *s = NULL; + + /* If a location is specified by envvar, try only that */ + s = getenv("NUT_AUTHCONF_FILE"); + if (s) { + if (stat(s, &st) == 0) { + filename = s; + goto found; + } + upsdebugx(5, "%s: tried to use requested '%s' but it was not there", __func__, s); + goto found; + } + + s = getenv("NUT_AUTHCONF_PATH"); + if (s) { + if (snprintf(fn, sizeof(fn), "%s/nutauth.conf", s) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to use requested '%s' but it was not there", __func__, fn); + } else { + upsdebugx(5, "%s: tried to use requested file under '%s' but could not construct the string", __func__, s); + } + goto found; + } + + s = getenv("HOME"); + if (s) { + if (snprintf(fn, sizeof(fn), "%s/.config/nut/nutauth.conf", s) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to default '%s' but it was not there", __func__, fn); + } + + if (snprintf(fn, sizeof(fn), "%s/.nutauth.conf", s) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to default '%s' but it was not there", __func__, fn); + } + } + + if (snprintf(fn, sizeof(fn), "%s/nutauth.conf", confpath()) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to default '%s' but it was not there", __func__, fn); + } + +found: + if (filename) { + upsdebugx(1, "%s: defaulted to %s", __func__, filename); + } else { + if (fatal_errors) { + fatalx(EXIT_FAILURE, "Can't open a user/site-provided default nutauth.conf file"); + } else { + upslogx(LOG_WARNING, "Can't open a user/site-provided default nutauth.conf file"); + return -1; + } + } + } + + return parse_authconf_file(filename, fatal_errors, 1); +} + +upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, const char *port) +{ + upsdebugx(2, "%s: starting for [%s]@[%s]:[%s]", __func__, NUT_STRARG(user), NUT_STRARG(host), NUT_STRARG(port)); + + if (!authconf_list) { + upsdebugx(2, "%s: returning %s: no list yet", + __func__, global_defaults ? "global defaults" : "NULL"); + return global_defaults; + } + + if (!host && !port && !user) { + /* Global section only */ + if (global_defaults) { + upsdebugx(2, "%s: returning global defaults: got no specific request", __func__); + return global_defaults; + } else { + /* Should not really get here AND succeed, + * fallback just in case */ + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + if (!tmp->section || !*(tmp->section)) { + upsdebugx(2, "%s: returning the section with NULL/empty name: got no specific request", __func__); + return tmp; + } + tmp = tmp->next; + } + } + upsdebugx(2, "%s: returning NULL: no global defaults were found, nor section with NULL name: got no specific request", __func__); + return NULL; + } else { + char *sect_user = (user ? xstrdup(user) : NULL), + *sect_host = (host ? xstrdup(host) : NULL), + *sect_port = (port ? xstrdup(port) : NULL), + *normalized_sect_name = NULL; + int fixed_sect_user = 0; + upscli_authconf_t *retval = global_defaults, *tmp = NULL; + + if (upscli_normalize_authconf_section_parts( + &normalized_sect_name, + §_user, + &fixed_sect_user, + §_host, + §_port) < 0 + ) { + upsdebugx(2, "%s: returning global defaults: could not upscli_normalize_authconf_section_parts()", __func__); + goto finished; /* return default */ + } + + /* 1. Try exactly the best info we have: user@host:port (user may be or not be empty) */ + tmp = authconf_list; + while (tmp) { + upsdebugx(4, "%s: matching '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, normalized_sect_name)) { + retval = tmp; + upsdebugx(2, "%s: returning a hit of '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + goto finished; + } + tmp = tmp->next; + } + + /* 2. Retry @host:port (host defaults) if that can help? */ + if (fixed_sect_user) { + const char *target_host_port = strchr(normalized_sect_name, '@'); + + if (target_host_port[1]) { + upsdebugx(4, "%s: retry with shorter '@host:port' for host defaults (without the user part)", __func__); + + tmp = authconf_list; + while (tmp) { + upsdebugx(4, "%s: matching '%s' against '%s'", __func__, target_host_port, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, target_host_port)) { + retval = tmp; + upsdebugx(2, "%s: returning a hit of '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + goto finished; + } + tmp = tmp->next; + } + } + } + + /* 3. Global defaults (section == NULL) */ + upsdebugx(2, "%s: returning global defaults: no more specific hit was found", __func__); + retval = global_defaults; + +finished: + free(sect_user); + free(sect_host); + free(sect_port); + free(normalized_sect_name); + return retval; + } +} + +upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, const char *port, int add_to_list) +{ + upscli_authconf_t *retval = global_defaults, *retval_user = NULL, *retval_host = NULL; + char *sect_user = (user ? xstrdup(user) : NULL), + *sect_host = (host ? xstrdup(host) : NULL), + *sect_port = (port ? xstrdup(port) : NULL), + *normalized_sect_name = NULL; + int fixed_sect_user = 0, created_item = 0; + + upsdebugx(2, "%s: starting for [%s]@[%s]:[%s]", __func__, NUT_STRARG(user), NUT_STRARG(host), NUT_STRARG(port)); + + /* We want this parsed always, so we can know if there + * is a fixed user, or assign the section name, at least */ + if (upscli_normalize_authconf_section_parts( + &normalized_sect_name, + §_user, + &fixed_sect_user, + §_host, + §_port) < 0 + ) { + upsdebugx(2, "%s: could not upscli_normalize_authconf_section_parts()", __func__); + } + upsdebugx(4, "%s: after normalization, proceeding for [%s]@[%s]:[%s] => '%s' (with%s fixed USER part)", + __func__, NUT_STRARG(sect_user), NUT_STRARG(sect_host), NUT_STRARG(sect_port), + NUT_STRARG(normalized_sect_name), fixed_sect_user ? "" : "out"); + + if (!authconf_list) { + upsdebugx(4, "%s: best match is %s: no list yet", + __func__, global_defaults ? "global defaults" : "NULL"); + goto found; + } + + if (!host && !port && !user) { + /* Global section only */ + if (global_defaults) { + upsdebugx(4, "%s: best match is global defaults: got no specific request", __func__); + goto found; + } else { + /* Should not really get here AND succeed, + * fallback just in case */ + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + if (!tmp->section || !*(tmp->section)) { + upsdebugx(4, "%s: best match is the section with NULL/empty name: got no specific request", __func__); + goto found; + } + tmp = tmp->next; + } + } + upsdebugx(4, "%s: best match is NULL: no global defaults were found, nor section with NULL name: got no specific request", __func__); + goto found; + } else { + const char *at = (fixed_sect_user ? strchr(normalized_sect_name, '@') : NULL); + upscli_authconf_t *tmp = authconf_list; + + while (tmp) { + upsdebugx(4, "%s: matching '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (tmp->section) { + if (!strcmp(tmp->section, normalized_sect_name)) { + if (fixed_sect_user) { + /* normalized_sect_name is user@host:port */ + retval_user = tmp; + upsdebugx(2, "%s: got exact user+host+port hit of '%s' against '%s'", + __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (retval_host) + break; + } else { + /* normalized_sect_name is @host:port */ + retval_host = tmp; + upsdebugx(2, "%s: got host+port hit of '%s' against '%s'", + __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + break; + } + } else + if (fixed_sect_user && !strcmp(tmp->section, at)) { + /* normalized_sect_name is user@host:port and we match '@host:port' */ + retval_host = tmp; + upsdebugx(2, "%s: got host+port hit of '%s' against '%s'", + __func__, at, NUT_STRARG(tmp->section)); + } + } + tmp = tmp->next; + } + + if (retval_user) { + retval = retval_user; + } else + if (retval_host) { + if (fixed_sect_user) + upsdebugx(4, "%s: did not find an exact user+host+port match in the list, only host+port", __func__); + retval = retval_host; + } else { + /* keep global_defaults or NULL, handle below */ + upsdebugx(4, "%s: did not find a match in the list", __func__); + } + } + +found: + if (!retval || retval == global_defaults) { + upsdebugx(2, "%s: best match from the list is %s", + __func__, global_defaults ? "global defaults" : "NULL"); + if (!global_defaults) { + upsdebugx(3, "%s: create new item for section '%s'", + __func__, normalized_sect_name); + retval = upscli_create_authconf_item(normalized_sect_name); + created_item = 1; + } else { + upsdebugx(3, "%s: clone new item for section '%s' from global_defaults", + __func__, normalized_sect_name); + retval = upscli_clone_authconf_item(global_defaults, normalized_sect_name); + created_item = 1; + } + } else { + if (!add_to_list || (!retval_user && fixed_sect_user)) { + upsdebugx(3, "%s: clone new item for section '%s' from '%s'", + __func__, normalized_sect_name, NUT_STRARG(retval->section)); + retval = upscli_clone_authconf_item(retval, normalized_sect_name); + created_item = 1; + } + } + + if (fixed_sect_user) { + free(retval->user); + retval->user = xstrdup(user); + } + + if (retval_user && retval_host) { + /* our retval is (maybe a clone of) retval_user */ +#ifdef DEBUG + upsdebugx(1, "merge user="); upscli_dump_authconf_item(NULL, retval, 1); + upsdebugx(1, "...and host="); upscli_dump_authconf_item(NULL, retval_host, 1); +#endif + upscli_merge_authconf_item(retval_host, retval); + } + + if ((retval_user || retval_host) && global_defaults) { + /* our retval is (maybe a clone of) retval_user or retval_host */ +#ifdef DEBUG + upsdebugx(1, "merge user/host="); upscli_dump_authconf_item(NULL, retval, 1); + upsdebugx(1, "...and globaldef="); upscli_dump_authconf_item(NULL, global_defaults, 1); +#endif + upscli_merge_authconf_item(global_defaults, retval); + } + +#ifdef DEBUG + upsdebugx(1, "final state ="); upscli_dump_authconf_item(NULL, retval, 1); +#endif + + if (add_to_list) { + if (created_item) { + upsdebugx(4, "%s: adding result to list", __func__); + upscli_add_authconf(retval); + } else { + upsdebugx(4, "%s: not adding result to list: edited existing item in-place", __func__); + } + } else { + upsdebugx(4, "%s: not adding result to list, caller must free it eventually", __func__); + } + + free(sect_user); + free(sect_host); + free(sect_port); + free(normalized_sect_name); + + return retval; +} diff --git a/clients/authconf.h b/clients/authconf.h new file mode 100644 index 0000000000..dfbcf55123 --- /dev/null +++ b/clients/authconf.h @@ -0,0 +1,124 @@ +/* authconf.h - prototypes and structures for NUT client authentication configuration + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef NUT_AUTHCONF_H_SEEN +#define NUT_AUTHCONF_H_SEEN 1 + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "nut_stdint.h" + +typedef struct upscli_authconf_s { + char *section; /* [@host:port] or [user@host:port], or NULL for global */ + char *user; + char *pass; + char *certpath; + char *certfile; + char *certident; + char *certpasswd; /* Password for key/cert storage */ + char *ssl_backend; /* openssl/nss */ + char *certhost; + int certverify; /* -1 = unset, 0 = off, 1 = on */ + int forcessl; /* -1 = unset, 0 = off, 1 = on */ + + struct upscli_authconf_s *next; +} upscli_authconf_t; + +/** Get the one global list of all parsed authentication configurations */ +upscli_authconf_t *upscli_get_authconf_list(void); + +/** Create a one-off configuration item, upscli_free_authconf_item() it manually */ +upscli_authconf_t *upscli_create_authconf_item(const char *section); + +/** Create a one-off configuration item, upscli_free_authconf_item() it manually */ +upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const char *section); + +/** Merge contents of two existing configuration items, they may be or not be on the list */ +upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_authconf_t *target); + +/** Free an authentication configuration item (if not NULL) and return its "next" pointer */ +upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node); + +/** Free the list of authentication configurations */ +void upscli_free_authconf_list(void); + +/** Read the authentication configuration file (usually nutauth.conf) + * If filename==NULL, tries to locate per-user ${HOME}/.config/nut/nutauth.conf + * and ${HOME}/.nutauth.conf, or site default ${nutconfdir}/nutauth.conf + * (whichever is found first); then one can follow `INCLUDE` trail if needed. + * Returns -1 on error, 1 on success + */ +int upscli_read_authconf_file(const char *filename, int fatal_errors); + +/** All p_* args must be non-NULL pointers to `char *` string variables + * which may be freed and re-allocated to return normalized values + * (original strings may themselves be NULL). + * The out_* values are optional and may be NULL if you do not want + * those data points returned. + */ +int upscli_normalize_authconf_section_parts( + char **out_normalized_sect_name, + char **p_sect_user, + int *out_fixed_sect_user, + char **p_sect_host, + char **p_sect_port); + +/** Take raw sect_name as input (e.g. a user-written string from config files). + * Normalize it by splitting into user, host, and port components (populating absent values). + * Return normalized components and reconstructed section name in output parameters (if not NULL), + * and 0 for successful completion or -1 if any error happened along the way. + */ +int upscli_split_authconf_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port); + +/** Find the best matching authconf for a given connection string in the list; + * if all args are NULL, return the global section or NULL if none such in the list. + */ +upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, const char *port); + +/** Find the best matching authconf for a given connection string, and fill in + * the missing points from higher levels (exact match => host defaults => global). + * Based on `add_to_list` flag, the returned item is always new and unique and + * not on the list (can adapt to changes in higher levels but must be freed by + * caller), or will be edited on or added to the list (subsequent calls would + * likely not add anything new, but memory management is easier, data is cached). + * if all args are NULL, return the global section or NULL if none such in the list. + */ +upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, const char *port, int add_to_list); + +/** Print one node to the specified stream (stdout if NULL), + * return code similar to fprintf() - sum of printed characters. + * + * The for_debug value controls the verbosity of the output: + * 0 - do not print NULL strings, do not indent global section + * 1 - print strings, indent global [] section as any other + * 2 - like 1, but do not escape special characters in strings (only double-quote them). + * + * Used from upscli_dump_authconf_list() */ +int upscli_dump_authconf_item(FILE *stream, upscli_authconf_t *node, int for_debug, int show_pass); + +/** Print ultimate configuration to the specified stream (stdout if NULL) + * and return the number of nodes in the current authconf list */ +size_t upscli_dump_authconf_list(FILE *stream, int for_debug, int show_pass); + +#ifdef __cplusplus +} +#endif + +#endif /* NUT_AUTHCONF_H_SEEN */ diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 1e85779db3..27b416a615 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2571,23 +2571,28 @@ SSLConfig_CERTHOST::SSLConfig_CERTHOST( const std::string& host_addr, const std::string& cert_subj, int forcessl, - int certverify) + int certverify, + uint16_t port) : _host_addr(host_addr), _cert_subj(cert_subj), _forcessl(forcessl), - _certverify(certverify) + _certverify(certverify), + _port(port) { + // TODO: Parse apart possible "host:port" spelling, involve getservbyname() } SSLConfig_CERTHOST::SSLConfig_CERTHOST( const char *host_addr, const char *cert_subj, int forcessl, - int certverify) + int certverify, + uint16_t port) : _host_addr(host_addr ? host_addr : ""), _cert_subj(cert_subj ? cert_subj : ""), _forcessl(forcessl), - _certverify(certverify) + _certverify(certverify), + _port(port) { } @@ -2610,6 +2615,11 @@ const char *SSLConfig_CERTHOST::getHostAddr_c_str() const return _host_addr.empty() ? nullptr : _host_addr.c_str(); } +uint16_t SSLConfig_CERTHOST::getPort() const +{ + return _port; +} + const std::string& SSLConfig_CERTHOST::getCertSubj() const { return _cert_subj; @@ -2632,7 +2642,10 @@ int SSLConfig_CERTHOST::getCertVerify() const bool SSLConfig_CERTHOST::operator < (const SSLConfig_CERTHOST& other) const { - if (_cert_subj.empty() && other._cert_subj.empty()) return _host_addr < other._host_addr; + if (_cert_subj.empty() && other._cert_subj.empty()) { + if (_host_addr == other._host_addr) return _port < other._port; + return _host_addr < other._host_addr; + } return _cert_subj < other._cert_subj; } @@ -2811,17 +2824,17 @@ const SSLConfig_CERTHOST *SSLConfig::getFirstCertHost() const return _certhosts.empty() ? nullptr : *(_certhosts.begin()); } -const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddr(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddr(const std::string &s, uint16_t port) const { for (const auto* item : _certhosts) { - if (item->getHostAddr() == s) { + if (item->getHostAddr() == s && (port == 0 || item->getPort() == 0 || item->getPort() == port)) { return item; } } return nullptr; } -const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(const std::string &s) const { for (const auto* item : _certhosts) { if (item->getCertSubj() == s) { @@ -2831,10 +2844,10 @@ const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(std::string &s) const return nullptr; } -const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddrOrSubj(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddrOrSubj(const std::string &s, uint16_t port) const { for (const auto* item : _certhosts) { - if (item->getHostAddr() == s || item->getCertSubj() == s) { + if ((item->getHostAddr() == s && (port == 0 || item->getPort() == 0 || item->getPort() == port)) || item->getCertSubj() == s) { return item; } } diff --git a/clients/nutclient.h b/clients/nutclient.h index e1c544aae9..e73ee53a57 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -402,17 +402,20 @@ class SSLConfig_CERTIDENT_NSS : public SSLConfig_CERTIDENT class SSLConfig_CERTHOST { public: + /** NOTE: Addr would be parsed into host:port and a 0 port may become NUT_PORT */ SSLConfig_CERTHOST( const std::string& host_addr, const std::string& cert_subj, int forcessl = -1, - int certverify = -1); + int certverify = -1, + uint16_t port = 0); SSLConfig_CERTHOST( const char *host_addr, const char *cert_subj, int forcessl = -1, - int certverify = -1); + int certverify = -1, + uint16_t port = 0); SSLConfig_CERTHOST& operator=(const SSLConfig_CERTHOST&) = default; SSLConfig_CERTHOST(const SSLConfig_CERTHOST&) = default; @@ -424,6 +427,8 @@ class SSLConfig_CERTHOST const std::string& getHostAddr() const; const char *getHostAddr_c_str() const; + uint16_t getPort() const; + const std::string& getCertSubj() const; const char *getCertSubj_c_str() const; @@ -438,6 +443,7 @@ class SSLConfig_CERTHOST std::string _cert_subj; int _forcessl; int _certverify; + uint16_t _port; }; /** @@ -496,9 +502,10 @@ class SSLConfig /** Simplify workflow for single-server connections */ const SSLConfig_CERTHOST *getFirstCertHost() const; - const SSLConfig_CERTHOST *getCertHostByAddr(std::string &s) const; - const SSLConfig_CERTHOST *getCertHostBySubj(std::string &s) const; - const SSLConfig_CERTHOST *getCertHostByAddrOrSubj(std::string &s) const; + /** NOTE: Addr would be parsed into host:port and a 0 port may become NUT_PORT */ + const SSLConfig_CERTHOST *getCertHostByAddr(const std::string &s, uint16_t port = 0) const; + const SSLConfig_CERTHOST *getCertHostBySubj(const std::string &s) const; + const SSLConfig_CERTHOST *getCertHostByAddrOrSubj(const std::string &s, uint16_t port = 0) const; /** Callback to apply this configuration into a TcpClient instance * (and further propagate into a Socket instance used by it). diff --git a/clients/upsc.c b/clients/upsc.c index c2c002a3b0..127d89c8f4 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -39,12 +39,14 @@ #define builtin_setproctag(x) setproctag(x) #define setproctag(x) do { builtin_setproctag(x); upscli_upslog_setproctag(x, nut_common_cookie()); } while(0) -static char *upsname = NULL, *hostname = NULL; +static char *upsname = NULL, *hostname = NULL, + /* Note: nutauth is either NULL or points to an optarg, so is not freed */ + *nutauth = NULL; static UPSCONN_t *ups = NULL; static int output_json = 0; /* For getopt loops below: */ -static const char optstring[] = "+DhlLcVW:j"; +static const char optstring[] = "+DhlLcVW:jA:"; static void help(const char *prog) { @@ -76,6 +78,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -D - raise debugging level\n"); printf(" -h - display this help text\n"); @@ -406,6 +411,8 @@ int main(int argc, char **argv) int varlist = 0, clientlist = 0, verbose = 0; const char *prog = getprogname_argv0_default(argc > 0 ? argv[0] : NULL, "upsc"); const char *net_connect_timeout = NULL; + int flags_ssl = UPSCLI_CONN_TRYSSL; + char str_port[16]; NUT_UNUSED_VARIABLE(upslog_start_tmp); upscli_upslog_setprocname(xstrdup(getmyprocname()), nut_common_cookie()); @@ -455,6 +462,11 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 'L': verbose = 1; goto fallthrough_case_l; @@ -489,6 +501,28 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + /* Not passing fatal_errors=1 into the parser due to JSON support */ + int parsed = -1; + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + parsed = upscli_read_authconf_file(NULL, 0); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + parsed = upscli_read_authconf_file(nutauth, 0); + } + if (parsed < 0) { + fatalx_error_json_simple(0, "Failed to parse auth config file"); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { char msg[LARGEBUF]; snprintf(msg, sizeof(msg), "invalid network timeout: %s", net_connect_timeout); @@ -516,9 +550,22 @@ int main(int argc, char **argv) upsdebugx(1, "upsname='%s' hostname='%s' port='%" PRIu16 "'", NUT_STRARG(upsname), NUT_STRARG(hostname), port); + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + ups = (UPSCONN_t *)xmalloc(sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { fatalx_error_json_simple(0, upscli_strerror(ups)); } diff --git a/clients/upsclient.c b/clients/upsclient.c index ed7ba58515..f3b411e319 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -157,13 +157,17 @@ static struct { typedef struct HOST_CERT_s { const char *host; + uint16_t port; const char *certname; int certverify; int forcessl; struct HOST_CERT_s *next; } HOST_CERT_t; +#if 0 static HOST_CERT_t* upscli_find_host_cert(const char* hostname); +#endif +static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t port, int verbose); /* Flag for SSL init */ static int upscli_initialized = 0; @@ -310,16 +314,89 @@ static SECStatus AuthCertificate(CERTCertDBHandle *arg, PRFileDesc *fd, PRBool checksig, PRBool isServer) { UPSCONN_t *ups = (UPSCONN_t *)SSL_RevealPinArg(fd); + CERTCertificate *peer = SSL_PeerCertificate(fd); SECStatus status = SSL_AuthCertificate(arg, fd, checksig, isServer); - upslogx(LOG_INFO, "Intend to authenticate server %s : %s", - ups?ups->host:"", - status==SECSuccess?"SUCCESS":"FAILED"); + upsdebugx(2, "%s: checking peer cert '%s' for NSS connection to URL '%s'", + __func__, + peer ? NUT_STRARG(peer->subjectName) : "", + NUT_STRARG(SSL_RevealURL(fd))); if (status != SECSuccess) { nss_error(isServer ? "SSL_AuthCertificate(server)" : "SSL_AuthCertificate(client)"); } + /* Check CERTHOST setting anticipated for host:port, if any + * Note that we keep "status" value as good or bad as the check above + * returned it, unless we make it worse by failing the test (or better + * once if we are told to ignore certverify results). + */ + if (peer && ups) { + HOST_CERT_t *cert; + + cert = upscli_find_host_port_cert(ups->host, ups->port, 1); + if (cert != NULL && cert->certname != NULL) { + upslogx(LOG_INFO, "Connecting in SSL to '%s' and look at certificate called '%s'", + ups->host, cert->certname); + + /* Note: CERT_VerifyCertName() is not necessarily + * what we want here, it focuses on domain names/URLs + * and we checked that for ups->host with generic + * SSL_AuthCertificate() above */ + upsdebugx(4, "%s: check if expected cert name matches peer by hostname rules", __func__); + if (CERT_VerifyCertName(peer, cert->certname) != SECSuccess) { + char *peer_subject_CN = (peer->subjectName ? (char*)strstr(peer->subjectName, "CN=") + 3 : NULL); + size_t certname_len = strlen(cert->certname); + + upsdebugx(4, "%s: check if expected cert name matches peer CN", __func__); + + /* Check if cert->certname matches the whole subject or just .../CN=.../ part as a string */ + if (!peer->subjectName || !( + strcmp(peer->subjectName, cert->certname) == 0 + || (peer_subject_CN && !strncmp(peer_subject_CN, cert->certname, certname_len) + && (peer_subject_CN[certname_len] == '\0' + || peer_subject_CN[certname_len] == '/' + || peer_subject_CN[certname_len] == ',' + || (peer_subject_CN[certname_len] == '\\' && peer_subject_CN[certname_len + 1] == '/')) ) + )) { + /* This way or that, the names differ */ + upslogx(LOG_ERR, "Peer certificate subject (%s) does not match CERTHOST name (%s)", + peer->subjectName ? peer->subjectName : "unknown", cert->certname); + + if (nut_debug_level > 4) + nss_error(isServer ? "CERT_VerifyCertName(server)" : "CERT_VerifyCertName(client)"); + + status = SECFailure; + } else { + upsdebugx(2, "Peer certificate subject verified against CERTHOST subject name (%s)", cert->certname); + } + } else { + upsdebugx(2, "Peer certificate subject verified against CERTHOST host name (%s)", cert->certname); + } + + if (status != SECSuccess) { + if (cert->certverify < 1) { + upslogx(LOG_ERR, "Peer certificate verification failed for '%s', but was not required, proceeding", ups->host); + status = SECSuccess; + } + } + } else { + upslogx(LOG_NOTICE, "Connecting in SSL to '%s' (no certificate name specified)", ups->host); + } + } else { + upsdebugx(1, "%s: WARNING: 'ups' pin arg and/or peer cert was NULL, who are we connecting to?", __func__); + } + + upslogx(LOG_INFO, "Intended to authenticate %s %s:%u : %s", + isServer ? "client" : "server", + ups ? ups->host : "", + (unsigned int)(ups ? ups->port : NUT_PORT), + status==SECSuccess ? "SUCCESS" : "FAILED"); + + if (peer) { + CERT_DestroyCertificate(peer); + } + return status; } @@ -331,8 +408,11 @@ static SECStatus AuthCertificateDontVerify(CERTCertDBHandle *arg, PRFileDesc *fd NUT_UNUSED_VARIABLE(checksig); NUT_UNUSED_VARIABLE(isServer); - upslogx(LOG_INFO, "Do not intend to authenticate server %s", - ups?ups->host:""); + upslogx(LOG_INFO, "Do not intend to authenticate %s %s:%u", + isServer ? "client" : "server", + ups ? ups->host : "", + (unsigned int)(ups ? ups->port : NUT_PORT)); + return SECSuccess; } @@ -341,15 +421,16 @@ static SECStatus BadCertHandler(UPSCONN_t *arg, PRFileDesc *fd) HOST_CERT_t* cert; NUT_UNUSED_VARIABLE(fd); - upslogx(LOG_WARNING, "Certificate validation failed for %s", - (arg&&arg->host)?arg->host:""); + upslogx(LOG_WARNING, "Certificate validation failed for %s:%" PRIu16, + (arg&&arg->host)?arg->host:"", + (uint16_t)(arg ? arg->port : NUT_PORT)); /* BadCertHandler is called when the NSS certificate validation is failed. * If the certificate verification (user conf) is mandatory, reject authentication * else accept it. */ - cert = upscli_find_host_cert(arg->host); + cert = arg ? upscli_find_host_port_cert(arg->host, arg->port, 1) : NULL; if (cert != NULL) { - return cert->certverify==0 ? SECSuccess : SECFailure; + return cert->certverify==0 ? SECSuccess : SECFailure; } else { return verify_certificate==0 ? SECSuccess : SECFailure; } @@ -739,6 +820,12 @@ static int openssl_cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) upsdebugx(5, "%s: issuer=%s", __func__, bufCA); } +/* TOTHINK: Match by CN=...? (Here we are after clearing other error cases) : + // This is the counterpart's own cert : + if (depth == 0 && !preverify_ok) { + } +*/ + if (openssl_cert_verify_data->always_continue) { upsdebugx(4, "%s: requested to always continue, return ok=1 (not %d provided by caller): depth=%d:%s", __func__, preverify_ok, depth, buf); return 1; @@ -750,13 +837,85 @@ static int openssl_cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) #endif -/* Legacy API, without support for client's own certificate in OpenSSL builds */ +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * Legacy API, without support for client's own certificate in OpenSSL builds. + * + * @see upscli_init_authconf() + * @see upscli_init2() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd) { return upscli_init2(certverify, certpath, certname, certpasswd, NULL); } +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * NOTE: Maybe eventually the upscli_init2()/upscli_init_authconf() methods + * will invert who is implementation of whom (the other being a wrapper). + * + * TODO: Consider a method that parses our collection from + * upscli_get_authconf_list() to upscli_add_host_port_cert() and + * set up the one most applicable set of client identity data + * for that [user@host:port] combo. + * + * @see upscli_init2() + * @see upscli_init() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ +int upscli_init_authconf(upscli_authconf_t *ac) +{ + if (!ac) { + upsdebugx(1, "%s: SKIP: NULL authconf pointer", __func__); + return -1; + } + + upsdebugx(5, "%s: got an authconf pointer", __func__); + if (nut_debug_level > 5) { + upscli_dump_authconf_item(stderr, ac, 1, 0); + } + + if (ac->certhost && ac->section) { + const char *host_port = strchr(ac->section, '@'); + + if (!host_port) { + host_port = ac->section; + } else { + host_port++; + } + + upscli_add_host_cert(host_port, ac->certhost, ac->certverify, ac->forcessl); + } + + return upscli_init2(ac->certverify, ac->certpath, ac->certident, ac->certpasswd, ac->certfile); +} + +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * Unlike legacy upscli_init() this method allows support for client's own + * certificate in OpenSSL builds (as well as NSS builds available before it). + * + * NOTE: Maybe eventually the upscli_init2()/upscli_init_authconf() methods + * will invert who is implementation of whom (the other being a wrapper). + * + * @see upscli_init_authconf() + * @see upscli_init() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ int upscli_init2(int certverify, const char *certpath, const char *certname, const char *certpasswd, const char *certfile) @@ -804,7 +963,10 @@ int upscli_init2(int certverify, const char *certpath, && strncmp(quiet_init_ssl, "TRUE", 4) && strncmp(quiet_init_ssl, "1", 1) ) ) { - upsdebugx(1, "NUT_QUIET_INIT_SSL='%s' value was not recognized, ignored", quiet_init_ssl); + if (strncmp(quiet_init_ssl, "false", 5) + && strncmp(quiet_init_ssl, "FALSE", 5) + && strncmp(quiet_init_ssl, "0", 1) ) + upsdebugx(1, "NUT_QUIET_INIT_SSL='%s' value was not recognized, ignored", quiet_init_ssl); quiet_init_ssl = NULL; } } @@ -1071,18 +1233,125 @@ int upscli_init2(int certverify, const char *certpath, return 1; } +static uint16_t get_port_from_string(const char *str_port) +{ + uint16_t retval = 0; + + if (str_port && *str_port) { +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_UNSIGNED_ZERO_COMPARE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_TYPE_LIMIT_COMPARE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS +# pragma GCC diagnostic ignored "-Wtype-limits" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE +# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_UNSIGNED_ZERO_COMPARE +# pragma GCC diagnostic ignored "-Wtautological-unsigned-zero-compare" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_TYPE_LIMIT_COMPARE +# pragma GCC diagnostic ignored "-Wtautological-type-limit-compare" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */ +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code" +#pragma clang diagnostic ignored "-Wtautological-compare" +#pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" +#endif + long l = atol(str_port); + + if (l > 0 && (uintmax_t)l <= (uintmax_t)UINT16_MAX) { + retval = (uint16_t)l; + } else { + struct servent *se = getservbyname(str_port, "tcp"); + if (se && se->s_port > 0 && (uintmax_t)(se->s_port) <= (uintmax_t)UINT16_MAX) { + retval = se->s_port; + } + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TYPE_LIMITS) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_UNSIGNED_ZERO_COMPARE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_TYPE_LIMIT_COMPARE) ) +# pragma GCC diagnostic pop +#endif + } + + return retval; +} + void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl) +{ + /* Support parsing apart authconf section names */ + const char *substr_port = strchr(hostname, ':'), *substr_host = strchr(hostname, '@'); + uint16_t port = NUT_PORT; + char host[LARGEBUF]; + + if (substr_host) { + substr_host++; + } else { + substr_host = hostname; + } + + if (substr_port) { + snprintf(host, + MIN(sizeof(host) - 1, (size_t)(substr_port - substr_host + 1)), + "%s", substr_host); + + if (substr_port[1]) { + port = get_port_from_string(substr_port + 1); + if (port == 0) { + upsdebugx(1, "%s: could not resolve port component '%s' " + "in [user@]hostname:port spec '%s' into a number, " + "falling back to standard NUT port", + __func__, hostname, substr_port + 1); + port = NUT_PORT; + } + } + } else { + host[0] = '\0'; + } + + upsdebugx(4, "%s: split '%s' into hostname '%s' port '%u'", + __func__, hostname, + substr_port ? host : substr_host, + (unsigned int)port); + + upscli_add_host_port_cert( + substr_port ? host : substr_host, + port, certname, certverify, forcessl); +} + +void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* certname, int certverify, int forcessl) { #if defined(WITH_OPENSSL) || defined(WITH_NSS) - HOST_CERT_t* cert = (HOST_CERT_t *)xmalloc(sizeof(HOST_CERT_t)); + HOST_CERT_t* cert = upscli_find_host_port_cert(hostname, port, 0); + + if (cert) { + upsdebugx(5, "%s: SKIP: found existing CERTHOST with same data (host '%s' and port '%u')", + __func__, hostname, (unsigned int)port); + return; + } + + cert = (HOST_CERT_t *)xmalloc(sizeof(HOST_CERT_t)); + + upsdebugx(1, "%s: adding CERTHOST: host '%s' port '%u' certname '%s' certverify %d forcessl %d", + __func__, hostname, (unsigned int)port, certname, certverify, forcessl); + cert->next = first_host_cert; cert->host = xstrdup(hostname); + cert->port = port ? port : NUT_PORT; cert->certname = xstrdup(certname); cert->certverify = certverify; cert->forcessl = forcessl; first_host_cert = cert; #else NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certverify); NUT_UNUSED_VARIABLE(forcessl); @@ -1091,26 +1360,66 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve #endif /* WITH_NSS */ } -static HOST_CERT_t* upscli_find_host_cert(const char* hostname) +static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t port, int verbose) { #if defined(WITH_OPENSSL) || defined(WITH_NSS) HOST_CERT_t* cert = first_host_cert; if (hostname != NULL) { while (cert != NULL) { - if (cert->host != NULL && strcmp(cert->host, hostname)==0 ) { + if (cert->host != NULL + && strcmp(cert->host, hostname) == 0 + && cert->port == port + ) { + if (verbose) + upsdebugx(4, "%s: found '%s' for '%s':'%u'", + __func__, NUT_STRARG(cert->certname), hostname, (unsigned int)port); return cert; } cert = cert->next; } } + if (verbose) + upsdebugx(4, "%s: nothing found for '%s':'%u'", __func__, hostname, (unsigned int)port); #else NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); - upsdebugx(4, "%s: no-op when libupsclient was not built WITH_SSL", __func__); + if (verbose) + upsdebugx(4, "%s: no-op when libupsclient was not built WITH_SSL", __func__); #endif /* WITH_OPENSSL | WITH_NSS */ return NULL; } +#if 0 +static HOST_CERT_t* upscli_find_host_cert(const char* hostname, int verbose) +{ + const char *substr_port = strchr(hostname, ':'); + uint16_t port = NUT_PORT; + char host[LARGEBUF]; + + if (substr_port) { + snprintf(host, + MIN(sizeof(host) - 1, (size_t)(substr_port - hostname)), + "%s", hostname); + + if (substr_port[1]) { + port = get_port_from_string(substr_port + 1); + if (port == 0) { + upsdebugx(1, "%s: could not resolve port component '%s' " + "in hostname:port spec '%s' into a number, " + "falling back to standard NUT port", + __func__, hostname, substr_port + 1); + port = NUT_PORT; + } + } + } + + return upscli_find_host_port_cert( + substr_port ? host : hostname, + port, verbose); +} +#endif + int upscli_cleanup(void) { #ifdef WITH_OPENSSL @@ -1135,6 +1444,7 @@ int upscli_cleanup(void) PL_ArenaFinish(); #endif /* WITH_NSS */ + upscli_free_authconf_list(); upscli_initialized = 0; return 1; } @@ -1569,10 +1879,15 @@ static ssize_t net_write(UPSCONN_t *ups, const char *buf, size_t buflen, const t # pragma GCC diagnostic pop #endif -/* - * 1 : OK - * -1 : ERROR - * 0 : SSL NOT SUPPORTED (whether by library or by server) +/** Initialize STARTTLS on the specified "ups" connection. + * + * For specific security requirements, you should call a + * method from the upscli_init() family in advance. + * + * Returns: + * - 1 : OK + * - -1 : ERROR + * - 0 : SSL NOT SUPPORTED (whether by library or by server) */ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) { @@ -1591,7 +1906,6 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) # elif defined(WITH_NSS) /* WITH_OPENSSL */ SECStatus status; PRFileDesc *socket; - HOST_CERT_t *cert; # endif /* WITH_OPENSSL | WITH_NSS */ char buf[UPSCLI_NETBUF_LEN]; @@ -1669,7 +1983,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) } { /* scoping */ - HOST_CERT_t *cert = upscli_find_host_cert(ups->host); + HOST_CERT_t *cert = upscli_find_host_port_cert(ups->host, ups->port, 1); if (cert != NULL && cert->certname != NULL) { /* We have a setting like upsmon CERTHOST - to pin the certificate @@ -1848,29 +2162,34 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) #endif if (verifycert) { status = SSL_AuthCertificateHook(ups->ssl, - (SSLAuthCertificate)AuthCertificate, CERT_GetDefaultCertDB()); + (SSLAuthCertificate)AuthCertificate, + CERT_GetDefaultCertDB()); } else { status = SSL_AuthCertificateHook(ups->ssl, - (SSLAuthCertificate)AuthCertificateDontVerify, CERT_GetDefaultCertDB()); + (SSLAuthCertificate)AuthCertificateDontVerify, + CERT_GetDefaultCertDB()); } if (status != SECSuccess) { nss_error("upscli_sslinit / SSL_AuthCertificateHook"); return -1; } - status = SSL_BadCertHook(ups->ssl, (SSLBadCertHandler)BadCertHandler, ups); + status = SSL_BadCertHook(ups->ssl, + (SSLBadCertHandler)BadCertHandler, ups); if (status != SECSuccess) { nss_error("upscli_sslinit / SSL_BadCertHook"); return -1; } - status = SSL_GetClientAuthDataHook(ups->ssl, (SSLGetClientAuthData)GetClientAuthData, ups); + status = SSL_GetClientAuthDataHook(ups->ssl, + (SSLGetClientAuthData)GetClientAuthData, ups); if (status != SECSuccess) { nss_error("upscli_sslinit / SSL_GetClientAuthDataHook"); return -1; } - status = SSL_HandshakeCallback(ups->ssl, (SSLHandshakeCallback)HandshakeCallback, ups); + status = SSL_HandshakeCallback(ups->ssl, + (SSLHandshakeCallback)HandshakeCallback, ups); if (status != SECSuccess) { nss_error("upscli_sslinit / SSL_HandshakeCallback"); return -1; @@ -1879,23 +2198,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) #pragma GCC diagnostic pop #endif - cert = upscli_find_host_cert(ups->host); - if (cert != NULL && cert->certname != NULL) { - upslogx(LOG_INFO, "Connecting in SSL to '%s' and look at certificate called '%s'", - ups->host, cert->certname); - - status = SSL_SetURL(ups->ssl, cert->certname); - if (status != SECSuccess) { - if (!(cert->certverify)) { - nss_error("upscli_sslinit / SSL_SetURL"); - upslogx(LOG_ERR, "Certificate verification failed for '%s', but was not required, proceeding", ups->host); - status = SSL_SetURL(ups->ssl, ups->host); - } - } - } else { - upslogx(LOG_NOTICE, "Connecting in SSL to '%s' (no certificate name specified)", ups->host); - status = SSL_SetURL(ups->ssl, ups->host); - } + status = SSL_SetURL(ups->ssl, ups->host); if (status != SECSuccess) { nss_error("upscli_sslinit / SSL_SetURL"); return -1; @@ -2154,7 +2457,7 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags ups->port = port; - hostcert = upscli_find_host_cert(host); + hostcert = upscli_find_host_port_cert(host, port, 1); if (hostcert != NULL) { /* An host security rule is specified. */ diff --git a/clients/upsclient.h b/clients/upsclient.h index 7be5c8b537..4f47c9151b 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -69,6 +69,7 @@ extern "C" { #define UPSCLI_NETBUF_LEN 512 /* network i/o buffer */ #include "parseconf.h" +#include "authconf.h" #ifdef WITH_OPENSSL /* Adapted from https://linux.die.net/man/3/ssl_set_verify man page example */ @@ -152,12 +153,15 @@ struct timeval *upscli_upslog_start_sync(struct timeval *tv, const void *cookie) * client certificate file. Equivalent to prefer upscli_init2(..., NULL) */ int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd); int upscli_init2(int certverify, const char *certpath, const char *certname, const char *certpasswd, const char *certfile); +int upscli_init_authconf(upscli_authconf_t *ac); int upscli_cleanup(void); int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags, struct timeval *tv); /* blocking unless default timeout is specified, see also: upscli_init_default_connect_timeout() */ int upscli_connect(UPSCONN_t *ups, const char *host, uint16_t port, int flags); +void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* certname, int certverify, int forcessl); +/* hostname may be a host:port */ void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl); /* --- functions that only use the new names --- */ diff --git a/clients/upscmd.c b/clients/upscmd.c index 5c14fb58e7..f3d48f8875 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -54,7 +54,7 @@ struct list_t { }; /* For getopt loops; should match usage documented below: */ -static const char optstring[] = "+Dlhu:p:t:wVW:"; +static const char optstring[] = "+Dlhu:p:t:wVW:A:"; static void help(const char *prog) { @@ -79,6 +79,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -D - raise debugging level\n"); printf(" -h - display this help text\n"); @@ -323,7 +326,8 @@ int main(int argc, char **argv) uint16_t port; ssize_t ret; int have_un = 0, have_pw = 0, cmdlist = 0; - char buf[SMALLBUF * 2], username[SMALLBUF], password[SMALLBUF]; + char buf[SMALLBUF * 2], username[SMALLBUF], password[SMALLBUF], *nutauth = NULL, str_port[16]; + int flags_ssl = UPSCLI_CONN_TRYSSL; const char *prog = getprogname_argv0_default(argc > 0 ? argv[0] : NULL, "upscmd"); const char *net_connect_timeout = NULL; @@ -375,6 +379,11 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 'l': cmdlist = 1; break; @@ -416,6 +425,23 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + upscli_read_authconf_file(NULL, 1); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + upscli_read_authconf_file(nutauth, 1); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { fatalx(EXIT_FAILURE, "Error: invalid network timeout: %s", net_connect_timeout); @@ -439,9 +465,22 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } diff --git a/clients/upsimage.c b/clients/upsimage.c index f9adbe8890..138469e837 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -626,8 +626,8 @@ static void clean_exit(void) int main(int argc, char **argv) { - char str[SMALLBUF], *s; - int i, min, nom, max; + char str[SMALLBUF], *s, str_port[16]; + int flags_ssl = UPSCLI_CONN_TRYSSL, i, min, nom, max; double var = 0; #ifdef WIN32 @@ -680,9 +680,25 @@ int main(int argc, char **argv) extractcgiargs(); + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + /* no 'host=' or 'display=' given */ if ((!monhost) || (!cmd)) noimage("No host or display"); @@ -699,7 +715,7 @@ int main(int argc, char **argv) #endif } - if (upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(&ups, hostname, port, flags_ssl) < 0) { noimage("Can't connect to server:\n%s\n", upscli_strerror(&ups)); #ifndef HAVE___ATTRIBUTE__NORETURN diff --git a/clients/upslog.c b/clients/upslog.c index 80977a0179..b5733f2a4d 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -195,7 +195,7 @@ static void help(const char *prog) __attribute__((noreturn)); /* For getopt loops; should match usage documented below: */ -static const char optstring[] = "+hDs:l:i:d:Nf:u:Vp:FBm:W:"; +static const char optstring[] = "+hDs:l:i:d:Nf:u:Vp:FBm:W:A:"; static void help(const char *prog) { @@ -233,6 +233,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -h - display this help text\n"); printf("\n"); printf("Some valid format string escapes:\n"); @@ -526,6 +529,9 @@ int main(int argc, char **argv) const char *net_connect_timeout = NULL; time_t now, nextpoll = 0; const char *user = NULL; + char *nutauth = NULL, str_port[16]; + upscli_authconf_t *ac_default = NULL; + int flags_ssl = UPSCLI_CONN_TRYSSL; struct passwd *new_uid = NULL; const char *pidfilebase = prog; /* For legacy single-ups -s/-l args: */ @@ -582,6 +588,11 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 'h': help(prog); #ifndef HAVE___ATTRIBUTE__NORETURN @@ -621,6 +632,7 @@ int main(int argc, char **argv) free(s); } /* var scope */ break; + case 's': monhost = optarg; break; @@ -705,6 +717,23 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + upscli_read_authconf_file(NULL, 1); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + upscli_read_authconf_file(nutauth, 1); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { fatalx(EXIT_FAILURE, "Error: invalid network timeout: %s", net_connect_timeout); @@ -797,12 +826,15 @@ int main(int argc, char **argv) if (!monhost_len) fatalx(EXIT_FAILURE, "No UPS defined for monitoring - use -s -l , or use -m ; consider -m '*,-' to view updates of all known local devices"); + ac_default = upscli_find_authconf_item(NULL, NULL, NULL); /* Split the system specs in a common fashion for tuples and legacy args */ for ( monhost_ups_current = monhost_ups_anchor, monhost_ups_prev = NULL; monhost_ups_current != NULL; monhost_ups_current = monhost_ups_current->next ) { + upscli_authconf_t *ac_current; + if (upscli_splitname(monhost_ups_current->monhost, &(monhost_ups_current->upsname), &(monhost_ups_current->hostname), &(monhost_ups_current->port)) != 0) { fatalx(EXIT_FAILURE, "Error: invalid UPS definition. Required format: upsname[@hostname[:port]]\n"); } @@ -814,6 +846,26 @@ int main(int argc, char **argv) monhost_ups_current->port ); + /* FIXME: Currently libupsclient allows for one SSL context shared + * by all connections, specifically the CERTIDENT of the client. + * We can have multiple CERTHOST certificates (and/or reading + * users/passwords) though. */ + ac_current = upscli_get_authconf_item(NULL, monhost_ups_current->hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, monhost_ups_current->port) > 0 ? str_port : NULL, 1); + /* Always call this, to register possible CERTHOSTs etc. */ + if (upscli_init_authconf(ac_current) > 0) { + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + // Do not call on the next loop cycle, if any + ac_default = NULL; + } + } + /* Revise the list if some UPS name was an asterisk * (query the data server) */ if (!strcmp(monhost_ups_current->upsname, "*")) { @@ -834,7 +886,7 @@ int main(int argc, char **argv) conn = (UPSCONN_t *)xmalloc(sizeof(*conn)); - if (upscli_connect(conn, monhost_ups_current->hostname, monhost_ups_current->port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(conn, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(conn)); } @@ -954,7 +1006,7 @@ int main(int argc, char **argv) monhost_ups_current->ups = (UPSCONN_t *)xmalloc(sizeof(UPSCONN_t)); - if (upscli_connect(monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, UPSCLI_CONN_TRYSSL) < 0) + if (upscli_connect(monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) fprintf(stderr, "Warning: initial connect failed: %s\n", upscli_strerror(monhost_ups_current->ups)); @@ -1039,7 +1091,7 @@ int main(int argc, char **argv) monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, - UPSCLI_CONN_TRYSSL); + flags_ssl); } run_flist(monhost_ups_current); diff --git a/clients/upsrw.c b/clients/upsrw.c index 636d868193..6a8ece2b4d 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -54,7 +54,7 @@ struct list_t { }; /* For getopt loops; should match usage documented below: */ -static const char optstring[] = "+Dhls:p:t:u:wVW:"; +static const char optstring[] = "+Dhls:p:t:u:wVW:A:"; static void help(const char *prog) { @@ -78,6 +78,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -D - raise debugging level\n"); printf(" -h - display this help text\n"); printf("\n"); @@ -683,7 +686,8 @@ int main(int argc, char **argv) uint16_t port; const char *prog = getprogname_argv0_default(argc > 0 ? argv[0] : NULL, "upsrw"); const char *net_connect_timeout = NULL; - char *password = NULL, *username = NULL, *setvar = NULL; + char *password = NULL, *username = NULL, *setvar = NULL, *nutauth = NULL, str_port[16]; + int flags_ssl = UPSCLI_CONN_TRYSSL; NUT_UNUSED_VARIABLE(upslog_start_tmp); upscli_upslog_setprocname(xstrdup(getmyprocname()), nut_common_cookie()); @@ -733,37 +737,50 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 's': setvar = optarg; break; + case 'l': if (setvar) { upslogx(LOG_WARNING, "Listing mode requested, overriding setvar specified earlier!"); setvar = NULL; } break; + case 'p': password = optarg; break; + case 't': if (!str_to_uint(optarg, &timeout, 10)) fatal_with_errno(EXIT_FAILURE, "Could not convert the provided value for timeout ('-t' option) to unsigned int"); break; + case 'u': username = optarg; break; + case 'w': tracking_enabled = 1; break; + case 'V': /* just show the version and optional * CONFIG_FLAGS banner if available */ print_banner_once(prog, 1); nut_report_config_flags(); exit(EXIT_SUCCESS); + case 'W': net_connect_timeout = optarg; break; + case 'h': default: help(prog); @@ -771,6 +788,23 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + upscli_read_authconf_file(NULL, 1); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + upscli_read_authconf_file(nutauth, 1); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { fatalx(EXIT_FAILURE, "Error: invalid network timeout: %s", net_connect_timeout); @@ -794,9 +828,22 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } diff --git a/clients/upsset.c b/clients/upsset.c index 20f70bf8b6..d446c3a984 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -52,6 +52,8 @@ struct list_t { /* network timeout for initial connection, in seconds */ #define UPSCLI_DEFAULT_CONNECT_TIMEOUT "10" +static int flags_ssl = UPSCLI_CONN_TRYSSL; + static char *monups, *username, *password, *function, *upscommand; /* set once the MAGIC_ENABLE_STRING is found in the upsset.conf */ @@ -371,7 +373,7 @@ static void upsd_connect(void) /* NOTREACHED */ } - if (upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(&ups, hostname, port, flags_ssl) < 0) { error_page("showsettings", "Connect failure", "Unable to connect to %s: %s", monups, upscli_strerror(&ups)); @@ -1124,8 +1126,8 @@ static void clean_exit(void) int main(int argc, char **argv) { - char *s; - int i; + char *s, str_port[16]; + int i; #ifdef WIN32 /* Required ritual before calling any socket functions */ @@ -1183,11 +1185,27 @@ int main(int argc, char **argv) /* see if the magic string is present in the config file */ check_conf(); + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); extractpostargs(); + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + /* Nothing POSTed (or parsed correctly)? */ if ((!username) || (!password) || (!function)) loginscreen(); diff --git a/clients/upsstats.c b/clients/upsstats.c index ab14107d51..979f5cad3e 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -40,6 +40,9 @@ /* network timeout for initial connection, in seconds */ #define UPSCLI_DEFAULT_CONNECT_TIMEOUT "10" +static upscli_authconf_t *ac_default = NULL; +static int flags_ssl = UPSCLI_CONN_TRYSSL; + static char *monhost = NULL; static int use_celsius = 1, refreshdelay = -1, treemode = 0; static int output_json = 0; @@ -477,6 +480,8 @@ static void ups_connect(void) static ulist_t *lastups = NULL; char *newups, *newhost; uint16_t newport = 0; + char str_port[16]; + upscli_authconf_t *ac_current = NULL; upsdebug_call_starting0(); @@ -535,11 +540,37 @@ static void ups_connect(void) exit(EXIT_FAILURE); } - if (currups && upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) - fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", currups->sys, upscli_strerror(&ups)); + /* FIXME: Currently libupsclient allows for one SSL context shared + * by all connections, specifically the CERTIDENT of the client. + * We can have multiple CERTHOST certificates (and/or reading + * users/passwords) though. */ + ac_current = upscli_get_authconf_item( + NULL, hostname, + snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, + 1); + /* Always call this, to register possible CERTHOSTs etc. */ + if (upscli_init_authconf(ac_current) > 0) { + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + // Do not call on the next loop cycle, if any + ac_default = NULL; + } + } + + if (currups && upscli_connect(&ups, hostname, port, flags_ssl) < 0) + fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", + currups ? NUT_STRARG(currups->sys) : "", + upscli_strerror(&ups)); lastups = currups; - upsdebug_call_finished2(": pick first device on newly connected data server [%s]", NUT_STRARG(currups->sys)); + upsdebug_call_finished2(": pick first device on newly connected data server [%s]", + currups ? NUT_STRARG(currups->sys) : ""); } static void do_hostlink(void) @@ -1690,7 +1721,7 @@ static void clean_exit(void) int main(int argc, char **argv) { char *s; - int i; + int i; #ifdef WIN32 /* Required ritual before calling any socket functions */ @@ -1747,9 +1778,15 @@ int main(int argc, char **argv) extractcgiargs(); + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); + /* Prepare for handling in first loop through ups_connect() */ + ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + /* * If json is in the query, bypass all HTML and call display_json() */ diff --git a/common/common.c b/common/common.c index ef9e68f71e..014b55b73d 100644 --- a/common/common.c +++ b/common/common.c @@ -43,9 +43,7 @@ #endif #include -#if !HAVE_DECL_REALPATH -# include -#endif +#include /* Just yield a unique value - e.g. address of a statically allocated variable * which would be different if several copies of NUT-common object code are @@ -613,6 +611,28 @@ pid_t get_max_pid_t(void) #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE #pragma GCC diagnostic pop #endif +} + +void check_perms(const char *fn) +{ +#ifndef WIN32 + int ret; + struct stat st; + + ret = stat(fn, &st); + + if (ret != 0) { + fatal_with_errno(EXIT_FAILURE, "stat %s", fn); + } + + /* include the x bit here in case we check a directory */ + if (st.st_mode & (S_IROTH | S_IXOTH)) { + upslogx(LOG_WARNING, "WARNING: %s is world readable (hope you don't have passwords there)", fn); + } +#else /* WIN32 */ + NUT_UNUSED_VARIABLE(fn); + NUT_WIN32_INCOMPLETE_MAYBE_NOT_APPLICABLE(); +#endif /* WIN32 */ } /* Normally sendsignalfn(), sendsignalpid() and related methods call @@ -5418,7 +5438,7 @@ void nut_prepare_search_paths(void) { upsdebugx(5, "%s: SKIP " "unreachable directory #%" PRIuSIZE " : %s", __func__, index, NUT_STRARG(dirname)); - index++; + index++; continue; } index++; diff --git a/conf/.gitignore b/conf/.gitignore index fb8a2fe90d..de6ede8acf 100644 --- a/conf/.gitignore +++ b/conf/.gitignore @@ -1,3 +1,4 @@ +/nutauth.conf.sample /upsmon.conf.sample /upssched.conf.sample /upsstats-single.html.sample diff --git a/conf/Makefile.am b/conf/Makefile.am index a3f49f9a6a..b8b9f4f6f8 100644 --- a/conf/Makefile.am +++ b/conf/Makefile.am @@ -7,7 +7,7 @@ INSTALL_0600 = $(INSTALL) -m 0600 # Note: ups.conf is a secured file, because for networked devices # it can contain SNMP, NetXML, IPMI or similar credentials SECFILES_STATIC = upsd.conf.sample upsd.users.sample ups.conf.sample -SECFILES_GENERATED = upsmon.conf.sample +SECFILES_GENERATED = upsmon.conf.sample nutauth.conf.sample PUBFILES_STATIC = nut.conf.sample PUBFILES_GENERATED = upssched.conf.sample CGIPUB_STATIC = hosts.conf.sample upsset.conf.sample @@ -31,7 +31,7 @@ nodist_conf_examples_DATA = $(SECFILES_GENERATED) $(PUBFILES_GENERATED) \ $(CGI_INSTALL_GENERATED) SPELLCHECK_SRC = $(dist_sysconf_DATA) \ - upssched.conf.sample.in upsmon.conf.sample.in \ + nutauth.conf.sample.in upssched.conf.sample.in upsmon.conf.sample.in \ upsstats-modern-list.html.sample.in upsstats-modern-single.html.sample.in \ upsstats.html.sample.in upsstats-single.html.sample.in @@ -55,6 +55,6 @@ SPELLCHECK_SRC = $(dist_sysconf_DATA) \ spellcheck spellcheck-interactive spellcheck-sortdict: @dotMAKE@ +$(MAKE) -f $(top_builddir)/docs/Makefile $(AM_MAKEFLAGS) MKDIR_P="$(MKDIR_P)" builddir="$(builddir)" srcdir="$(srcdir)" top_builddir="$(top_builddir)" top_srcdir="$(top_srcdir)" SPELLCHECK_SRC="$(SPELLCHECK_SRC)" SPELLCHECK_SRCDIR="$(srcdir)" SPELLCHECK_BUILDDIR="$(builddir)" $@ - +# WARNING: Do not clean away files generated from templates by configure script! MAINTAINERCLEANFILES = Makefile.in .dirstamp CLEANFILES = *.pdf *.html *-spellchecked diff --git a/conf/nutauth.conf.sample.in b/conf/nutauth.conf.sample.in new file mode 100644 index 0000000000..11afa19600 --- /dev/null +++ b/conf/nutauth.conf.sample.in @@ -0,0 +1,48 @@ +# The `nutauth.conf` file conveys information needed for NUT clients to +# authenticate themselves to a NUT data server, as well as to validate +# that this is a server they want to trust (when SSL/TLS mode is used). +# +# By default, these files are located in either a per-user location like +# `${HOME}/.config/nut/nutauth.conf` or `${HOME}/.nutauth.conf`, or a +# site default `${NUT_CONFDIR}/nutauth.conf` (whichever is found first). +# Such a file may `INCLUDE` further configurations (e.g. hop from a +# per-user file to load server-wide defaults) if desired. +# +# While it usually suffices to have one client certificate for all servers, +# it may be that some remote system owned/managed by a different department +# would insist on *themselves* issuing (and revoking) certificates for their +# equipment. In this case, you can specify connection-specific settings. + +# Example contents: +# +# # Global section, inherited and overridden per line by others: +# CERTPATH = @CONFPATH@/certs/client +# # CERTFILE is only relevant for OpenSSL: +# CERTFILE = @CONFPATH@/certs/client/nut-client-hostname.pem +# CERTIDENT_NAME = "NUT Client" +# CERTIDENT_PASS = "KeyP@$$phrase!" +# # -1 for inheriting a better value elsewhere, e.g. in some host +# # definition below, or effectively 0 if never defined exactly +# # 1 to require the feature to succeed +# # (CV) 0 for not verifying server certificate against trusted CA +# CERTVERIFY = 1 +# # (FS) 0 for OK to fail STARTTLS => proceed in plaintext +# FORCESSL = -1 +# +# [@localhost] +# USER = "admin" +# PASS = "Adm1n!Pass" +# +# [upsmonuser@localhost] +# PASS = "monitor" +# # Note you can not override USER in this context, +# # where it is part of section name +# +# [@remote-host:34935] +# USER = "other-user" +# PASS = "other_pass" +# CERTPATH = @CONFPATH@/certs/client-for-remote-host +# CERTIDENT_NAME = "NUT Client for remote host" +# CERTIDENT_PASS = "C00lP@$$" +# CERTVERIFY = 1 +# FORCESSL = 1 diff --git a/conf/upsmon.conf.sample.in b/conf/upsmon.conf.sample.in index f07e7a4d40..ff5abecceb 100644 --- a/conf/upsmon.conf.sample.in +++ b/conf/upsmon.conf.sample.in @@ -661,6 +661,9 @@ ALARMCRITICAL 1 # # CERTHOST localhost "My nut server" 1 1 # +# For a data server with non-default port, it should be specified, e.g. +# CERTHOST "localhost:34930" "My nut server" 1 1 +# # See 'docs/security.txt' or the Security chapter of NUT user manual # for more information on the SSL support in NUT. diff --git a/configure.ac b/configure.ac index 028db966c1..55a52a880e 100644 --- a/configure.ac +++ b/configure.ac @@ -7282,6 +7282,7 @@ AC_CONFIG_FILES([ clients/Makefile common/Makefile conf/Makefile + conf/nutauth.conf.sample conf/upsmon.conf.sample conf/upssched.conf.sample conf/upsstats.html.sample diff --git a/docs/Makefile.am b/docs/Makefile.am index cb583a0654..f33a9d6d53 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -865,6 +865,11 @@ SPELLCHECK_ENV_DEBUG = no ASPELL_NUT_COMMON_ARGS = -p $(abs_srcdir)/$(NUT_SPELL_DICT) ASPELL_NUT_COMMON_ARGS += -d en --lang=en --ignore-accents ASPELL_NUT_COMMON_ARGS += --encoding=utf-8 +# Do not let it remove words it thinks are not needed (in interactive mode) -- +# default dictionaries differ from platform to platform, from decade to decade: +ASPELL_NUT_COMMON_ARGS += --dont-validate-words +ASPELL_NUT_COMMON_ARGS += --dont-clean-words +ASPELL_NUT_COMMON_ARGS += --dont-clean-affixes # Note: If there is a need to use filter path (e.g. in mingw/msys2 builds), # it must be before --mode=tex (-t) option! ASPELL_NUT_TEXMODE_ARGS = diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index abe8bd0e22..ac625f51b4 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -245,6 +245,7 @@ check-list-SRC_ALL_PAGES: # Base configuration and client manpages, always installed SRC_CONF_PAGES = \ nut.conf.txt \ + nutauth.conf.txt \ ups.conf.txt \ upsd.conf.txt \ upsd.users.txt \ @@ -253,6 +254,7 @@ SRC_CONF_PAGES = \ INST_MAN_CONF_PAGES = \ nut.conf.$(MAN_SECTION_CFG) \ + nutauth.conf.$(MAN_SECTION_CFG) \ ups.conf.$(MAN_SECTION_CFG) \ upsd.conf.$(MAN_SECTION_CFG) \ upsd.users.$(MAN_SECTION_CFG) \ @@ -269,6 +271,7 @@ mancfg_DATA += $(MAN_CONF_PAGES) INST_HTML_CONF_MANS = \ nut.conf.html \ + nutauth.conf.html \ ups.conf.html \ upsd.conf.html \ upsd.users.html \ @@ -553,6 +556,12 @@ SRC_DEV_PAGES = \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_upslog_set_debug_level.txt \ + upscli_create_authconf_item.txt \ + upscli_dump_authconf_item.txt \ + upscli_find_authconf_item.txt \ + upscli_free_authconf_list.txt \ + upscli_get_authconf_list.txt \ + upscli_read_authconf_file.txt \ upscli_str_add_unique_token.txt \ upscli_str_contains_token.txt \ libnutclient.txt \ @@ -721,6 +730,7 @@ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS): upscli_upslog_set_debug_level.$(MAN_SECTI INST_MAN_DEV_API_PAGES = \ upsclient.$(MAN_SECTION_API) \ upscli_add_host_cert.$(MAN_SECTION_API) \ + $(UPSCLI_ADD_HOST_CERT_DEPS) \ upscli_cleanup.$(MAN_SECTION_API) \ upscli_connect.$(MAN_SECTION_API) \ upscli_tryconnect.$(MAN_SECTION_API) \ @@ -749,6 +759,15 @@ INST_MAN_DEV_API_PAGES = \ upscli_strerror.$(MAN_SECTION_API) \ upscli_upserror.$(MAN_SECTION_API) \ upscli_upslog_set_debug_level.$(MAN_SECTION_API) \ + upscli_create_authconf_item.$(MAN_SECTION_API) \ + $(UPSCLI_CREATE_AUTHCONF_DEPS) \ + upscli_dump_authconf_item.$(MAN_SECTION_API) \ + $(UPSCLI_DUMP_AUTHCONF_DEPS) \ + upscli_find_authconf_item.$(MAN_SECTION_API) \ + $(UPSCLI_FIND_AUTHCONF_DEPS) \ + upscli_free_authconf_list.$(MAN_SECTION_API) \ + upscli_get_authconf_list.$(MAN_SECTION_API) \ + upscli_read_authconf_file.$(MAN_SECTION_API) \ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS) \ upscli_str_add_unique_token.$(MAN_SECTION_API) \ upscli_str_contains_token.$(MAN_SECTION_API) \ @@ -797,10 +816,14 @@ INST_MAN_DEV_API_PAGES = \ nutscan_init.$(MAN_SECTION_API) # Alias page for one text describing two commands: -UPSCLI_INIT_DEPS = upscli_init2.$(MAN_SECTION_API) +UPSCLI_INIT_DEPS = upscli_init2.$(MAN_SECTION_API) upscli_init_authconf.$(MAN_SECTION_API) $(UPSCLI_INIT_DEPS): upscli_init.$(MAN_SECTION_API) touch $@ +UPSCLI_ADD_HOST_CERT_DEPS = upscli_add_host_port_cert.$(MAN_SECTION_API) +$(UPSCLI_ADD_HOST_CERT_DEPS): upscli_add_host_cert.$(MAN_SECTION_API) + touch $@ + upscli_readline_timeout.$(MAN_SECTION_API): upscli_readline.$(MAN_SECTION_API) touch $@ @@ -816,6 +839,27 @@ upscli_sendline_timeout_may_disconnect.$(MAN_SECTION_API): upscli_sendline.$(MAN upscli_tryconnect.$(MAN_SECTION_API): upscli_connect.$(MAN_SECTION_API) touch $@ +UPSCLI_CREATE_AUTHCONF_DEPS = \ + upscli_clone_authconf_item.$(MAN_SECTION_API) \ + upscli_merge_authconf_item.$(MAN_SECTION_API) \ + upscli_free_authconf_item.$(MAN_SECTION_API) + +UPSCLI_DUMP_AUTHCONF_DEPS = upscli_dump_authconf_list.$(MAN_SECTION_API) + +UPSCLI_FIND_AUTHCONF_DEPS = \ + upscli_get_authconf_item.$(MAN_SECTION_API) \ + upscli_normalize_authconf_section_parts.$(MAN_SECTION_API) \ + upscli_split_authconf_section.$(MAN_SECTION_API) + +$(UPSCLI_CREATE_AUTHCONF_DEPS): upscli_create_authconf_item.$(MAN_SECTION_API) + touch $@ + +$(UPSCLI_DUMP_AUTHCONF_DEPS): upscli_dump_authconf_item.$(MAN_SECTION_API) + touch $@ + +$(UPSCLI_FIND_AUTHCONF_DEPS): upscli_find_authconf_item.$(MAN_SECTION_API) + touch $@ + nutscan_scan_ip_range_snmp.$(MAN_SECTION_API): nutscan_scan_snmp.$(MAN_SECTION_API) touch $@ @@ -890,6 +934,12 @@ INST_HTML_DEV_MANS = \ upscli_strerror.html \ upscli_upserror.html \ upscli_upslog_set_debug_level.html \ + upscli_create_authconf_item.html \ + upscli_dump_authconf_item.html \ + upscli_find_authconf_item.html \ + upscli_free_authconf_list.html \ + upscli_get_authconf_list.html \ + upscli_read_authconf_file.html \ upscli_str_add_unique_token.html \ upscli_str_contains_token.html \ libnutclient.html \ @@ -957,6 +1007,21 @@ upscli_sendline_timeout.html upscli_sendline_timeout_may_disconnect.html: upscli upscli_tryconnect.html: upscli_connect.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ +upscli_get_authconf_item.html: upscli_find_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_normalize_authconf_section_parts.html: upscli_find_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_split_authconf_section.html: upscli_find_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_free_authconf_item.html: upscli_create_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_dump_authconf_list.html: upscli_dump_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + nutscan_scan_ip_range_snmp.html: nutscan_scan_snmp.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt new file mode 100644 index 0000000000..2cb2761e43 --- /dev/null +++ b/docs/man/nutauth.conf.txt @@ -0,0 +1,196 @@ +NUTAUTH.CONF(5) +=============== + +NAME +---- + +nutauth.conf - Authentication and SSL configuration for NUT clients + +DESCRIPTION +----------- + +This file is read by the NUT client library linkman:libupsclient[3] via +linkman:upscli_read_authconf_file[3]. It allows users to define default and +per-server authentication credentials (username and password) and SSL/TLS +settings (certificates, verification, etc.) for use by NUT clients like +linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], and others. +Note that there is a dedicated linkman:upsmon.conf[5] configuration file +for the linkman:upsmon[8] client. + +This file begins with optional global directives which can provide defaults +for all connections. Per-server or per-account sections can then be defined +to override these defaults. + +SECTION TITLES +~~~~~~~~~~~~~~ + +A section begins with a name in brackets. Sections are matched against the +server being contacted. Supported section formats are: + +*[@host:port]*:: + Defines defaults for a specific host and port. + +*[@host]*:: + Defines defaults for a specific host (uses the default NUT port + as defined at build configuration time, implicit default is `3493`). + +*[user@host:port]*:: + Defines credentials and settings for a specific user on a specific + host and port. The `USER` directive would be ignored in this entry. + +*[user@host]*:: + Defines credentials and settings for a specific user on a specific + host (uses the default NUT port). + +*[host]*:: + A section title without an `@` character effectively defines defaults + for a specific host and default NUT port, just because this is more + likely than such a string being a specified "user" at an implicit + "localhost". ++ +Consequently, titles with a colon `:` without an `@` are interpreted + as *[host:port]* (or *[:port]* with an implicit "localhost"). + +Section titles are normalized when parsing the `nutauth.conf` file(s), +so the inclusion order (nesting) and run-time search for best-match +configuration can be deterministic. + +For example: + +* an empty 'host' component value is interpreted as `localhost`, e.g. + in `[@:port]` spelling; +* a non-numeric 'port' string would be resolved in the OS service naming + database, if possible, e.g. in `[@localhost:nut]` spelling; +* an empty or absent 'port' component is understood as using the default + NUT port (as detailed above for `[@host]` spelling). + +A section continues until the next section header or until EOF. + +The special names `[]` and `[_global_defaults]` are reserved and should +not normally be used as a section header; just use the global scope (lines +before any section header) for default settings. These reserved explicit +names can however be helpful to maintain order in nested files that you +can `INCLUDE` in your top-level `nutauth.conf`. + +Note that lines which seem like the global scope in an included configuration +(not preceded by any section title) would modify the parent section; however +lines after an explicit section title -- even the `[_global_defaults]` +one -- would not. + +OTHER FORMAT CONSIDERATIONS +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuration directives are specified as `KEY = VALUE` pairs; simple string +values may be passed "as is", but those with spaces or other special characters +should be double-quoted and/or use escape sequences, like in other NUT files. + +Blank lines and characters after an un-quoted hash (`#`) are ignored. + +Example: + + # Global defaults + CERTVERIFY = on + FORCESSL = on + + # Host defaults for a local server + [@localhost] + PASS = "password1" + + # Specific account on a remote server + [admin@remoteserver:3493] + PASS = "secret2" + CERTVERIFY = off + +IMPORTANT NOTES +--------------- + +* Contents of this file should be pure ASCII. +* Permissions should be restricted (e.g., `chmod 600`) since this file + contains passwords. + +GLOBAL AND SECTION DIRECTIVES +----------------------------- + +The following keywords are supported in both global scope and within sections: + +*user* or *username* (case-insensitive token):: + Optional. Specify the NUT username for authentication (as defined by + linkman:upsd.users[5] on the data server side). If the section header + already specified a user (e.g., `[user@host]`), this keyword is ignored + within that section. + +*pass* or *password* (case-insensitive token):: + Optional. Specify the NUT password for authentication. + +*CERTPATH*:: + Optional. Specify the path to trusted CA certificates (e.g., for + verifying the server's certificate), otherwise the system default + CA certificate store is used. In case of NSS, this is the path to + location of the NSS DB files used for all purposes. + +*CERTFILE*:: + Optional (OpenSSL only). Specify the client certificate file for + client-side authentication to the server. The PEM file should start + with the individual certificate, followed by the chain of certificates + of authorities that issued it, and finished by the private key. + +*CERTIDENT_NAME*:: + Optional. Specify the client certificate identity (nickname, alias). + +*CERTIDENT_PASS*:: + Optional. Specify the password to decrypt the client's private key. + +*SSLBACKEND*:: + Optional. Specify the SSL/TLS backend to use (e.g., `openssl` or `nss`), + if the default is not suitable. + +*CERTHOST*:: + Optional. Specify the expected certificate subject (common name) of that + server's certificate; alternately the IP address or host name used in + the section title should match that in the common name (CN) or subject + alternate names (SAN). + +*CERTVERIFY*:: + Optional. Enable or disable server certificate verification. + Accepted values: `on`, `yes`, `1` (enable) or `off`, `no`, `0` (disable). + +*FORCESSL*:: + Optional. Require SSL/TLS for the connection. + Accepted values: `on`, `yes`, `1` (enable) or `off`, `no`, `0` (disable). + +NESTING (INCLUDE FILES) +----------------------- + +Included files are supported via the `INCLUDE` directive for optionally +present files, and `INCLUDE_REQUIRED` for files that must be there +(otherwise the program exits with a fatal error). + +Global-scope includes may modify global default items, as well as define +new sections or overlay items in existing sections. + +Section-scope includes (nested within a section) can only modify data +within that section. They do not require a section title (effectively the +global section of the included file modifies the section of the parent +file it was included into), but if these files do have any section title(s), +then contents of sections that after normalization do not match the section +title of the parent file would be skipped. + +Example: + + INCLUDE_REQUIRED /etc/nut/nutauth-defaults.conf + + [@localhost] + INCLUDE /etc/nut/nutauth-local.conf + +SEE ALSO +-------- + +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], +linkman:upsd.users[5], +linkman:upsmon.conf[5], linkman:upsmon[8] + +Internet resources +~~~~~~~~~~~~~~~~~~ + +The NUT (Network UPS Tools) home page: https://www.networkupstools.org/ diff --git a/docs/man/upsc.txt b/docs/man/upsc.txt index 215ffc25c5..f26ee0b3bd 100644 --- a/docs/man/upsc.txt +++ b/docs/man/upsc.txt @@ -41,13 +41,13 @@ OPTIONS *-l* 'host':: List all UPS names configured at 'host', one name per line. The hostname - defaults to "localhost". You may optionally add a colon and a port number. + defaults to `localhost`. You may optionally add a colon and a port number. *-L* 'host':: As above, list all UPS names configured at 'host', including their description provided by the remote linkman:upsd[8] from its linkman:ups.conf[5]. - The hostname defaults to "localhost". You may optionally add a colon and + The hostname defaults to `localhost`. You may optionally add a colon and a port number to override the default port. *-c* 'ups':: @@ -57,7 +57,7 @@ OPTIONS 'ups':: Display the status of that UPS. The format for this option is - 'upsname[@hostname[:port]]'. The default hostname is "localhost". + 'upsname[@hostname[:port]]'. The default hostname is `localhost`. 'variable':: @@ -89,6 +89,27 @@ COMMON OPTIONS indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + EXAMPLES -------- @@ -162,6 +183,7 @@ SEE ALSO linkman:upslog[8], linkman:ups.conf[5], +linkman:nutauth.conf[5], linkman:upsd[8] Internet resources: diff --git a/docs/man/upscli_add_host_cert.txt b/docs/man/upscli_add_host_cert.txt index f5f2dcb79e..dcdf49d5d5 100644 --- a/docs/man/upscli_add_host_cert.txt +++ b/docs/man/upscli_add_host_cert.txt @@ -4,7 +4,8 @@ UPSCLI_ADD_HOST_CERT(3) NAME ---- -upscli_add_host_cert - Register a security rule for an host. +upscli_add_host_cert, upscli_add_host_port_cert - Register +a security rule for a host. SYNOPSIS -------- @@ -17,13 +18,25 @@ SYNOPSIS const char* certname, int certverify, int forcessl); + + void upscli_add_host_port_cert( + const char* hostname, + uint16_t port, + const char* certname, + int certverify, + int forcessl) ------ DESCRIPTION ----------- The *upscli_add_host_cert()* function registers a security rule associated -to the 'hostname'. All connections to this host use this rule. +to the 'hostname' (may spell out a `host:port` in fact). +The *upscli_add_host_port_cert()* function registers a security rule +associated to the exact 'hostname' and 'port' number. +If an entry with the same exact `hostname` and `port` values already exists, +it is not replaced. +All connections to this host use this rule. The rule is composed of the certificate name 'certname' expected for the host, 'certverify' if the certificate must be validated for the host diff --git a/docs/man/upscli_create_authconf_item.txt b/docs/man/upscli_create_authconf_item.txt new file mode 100644 index 0000000000..2040441c74 --- /dev/null +++ b/docs/man/upscli_create_authconf_item.txt @@ -0,0 +1,97 @@ +UPSCLI_CREATE_AUTHCONF_ITEM(3) +============================== + +NAME +---- + +upscli_create_authconf_item, upscli_clone_authconf_item, +upscli_merge_authconf_item, upscli_free_authconf_item - Create +or free an individual authentication configuration item, +which is not necessarily in the globally tracked list + +SYNOPSIS +-------- + +------ + #include + + typedef struct upscli_authconf_s { + char *section; /* [@host:port] or [user@host:port], or NULL for global */ + char *user; + char *pass; + char *certpath; + char *certfile; + char *certident; + char *certpasswd; /* Password for key/cert storage */ + char *ssl_backend; /* openssl/nss */ + char *certhost; + int certverify; /* -1 = unset, 0 = off, 1 = on */ + int forcessl; /* -1 = unset, 0 = off, 1 = on */ + + struct upscli_authconf_s *next; + } upscli_authconf_t; + + upscli_authconf_t *upscli_create_authconf_item(const char *section); + + upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const char *section); + + upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_authconf_t *target); + + upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node); +------ + +DESCRIPTION +----------- + +The *upscli_create_authconf_item()* function allocates the memory for an +`upscli_authconf_t` node and initializes it with the given section name. +Other string pointers remain `NULL`, numeric flags are set to `-1`, and +the `next` pointer is set to `NULL`. This item is not automatically added +to the list. + +The *upscli_clone_authconf_item()* function allocates the memory for an +`upscli_authconf_t` node and initializes it with the given section name +(if not NULL) and clones the values from the source node to the new node +by re-allocating non-NULL strings with `xstrdup()` and copying the numeric +flags. If the 'section' argument is provided and contains a non-trivial +`user@` component, the 'user' field if the structure is (re-)set to that +value. The `next` pointer is set to `NULL`. + +The *upscli_merge_authconf_item()* function copies the values from the +'source' node to the 'target' node, as long as the target node does not +already have a value for the same field (non-NULL possibly empty string, +or a non-negative numeric flag). The `next` pointer is not modified. +Like above, if the resulting section name in 'target' contains the `@` +character and some before it, the 'user' name field would be (re-)set +to that value and not cloned. + +Typically, the 'source' node when merging is the global configuration item +(or a `host:port` when merging for a section with specific user value), +and the 'target' node is a specific `host:port` or `user@host:port` +configuration item. + +The *upscli_free_authconf_item()* function frees the memory allocated for +a single `upscli_authconf_t` node and returns its `next` pointer. This is +useful for manually iterating and cleaning up copies of the list, although +typically linkman:upscli_free_authconf_list[3] is used to clear the entire +internal list. + +RETURN VALUE +------------ + +The *upscli_create_authconf_item()* and *upscli_clone_authconf_item()* +functions returns a pointer to the new structure, or `NULL` if an error +occurred. + +The *upscli_merge_authconf_item()* function returns a pointer to the merged +'target' structure. + +The *upscli_free_authconf_item()* function returns a pointer to the next element +of the `upscli_authconf_t` list, if known (and if it was part of the list). + +SEE ALSO +-------- + +linkman:upscli_dump_authconf_item[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_get_authconf_list[3], linkman:upscli_dump_authconf_list[3] diff --git a/docs/man/upscli_dump_authconf_item.txt b/docs/man/upscli_dump_authconf_item.txt new file mode 100644 index 0000000000..91129905db --- /dev/null +++ b/docs/man/upscli_dump_authconf_item.txt @@ -0,0 +1,68 @@ +UPSCLI_DUMP_AUTHCONF_ITEM(3) +============================ + +NAME +---- + +upscli_dump_authconf_item, upscli_dump_authconf_list - Print authentication configuration node or list + +SYNOPSIS +-------- + +------ + #include + + int upscli_dump_authconf_item(FILE *stream, + upscli_authconf_t *node, int for_debug, int show_pass); + + size_t upscli_dump_authconf_list(FILE *stream, + int for_debug, int show_pass); +------ + +DESCRIPTION +----------- + +The *upscli_dump_authconf_item()* function prints a single configuration 'node' +to the specified 'stream'. If 'stream' is NULL, it defaults to `stdout`. + +The *upscli_dump_authconf_list()* function prints the entire internal list of +authentication configurations to the specified 'stream'. + +These functions are primarily intended for debugging purposes to verify the +content of the parsed configuration. + +*for_debug*:: +If 'for_debug' is '0', 'NULL' strings are not dumped, and the global section +(a 'node' with 'NULL' or empty 'section' field) is not indented. String +contents are printed in double-quotes with appropriate encoding to escape +characters special for NUT configuration parser. Theoretically, this could +be used to populate a conforming NUT auth configuration file. ++ +If 'for_debug' is '1', 'NULL' strings are dumped as unquoted ``, and +the global section is titled as `[]` and indented like any other. +String contents are also printed in double-quotes with appropriate encoding. ++ +If 'for_debug' is '2', behavior is like with '1' except that string contents +are printed in double-quotes but otherwise as they were (result may be invalid +for subsequent re-parsing, if there are unfortunate combinations of special +characters). + +*show_pass*:: +If 'show_pass' is '0', `` would be shown in case of non-NULL strings +for user and private key passwords. Set it to '1' to show such sensitive data +(ideally just in test programs, not in regular clients). + +RETURN VALUE +------------ + +The *upscli_dump_authconf_item()* function returns the return value of the +underlying linkman:fprintf[3] call, or -1 if 'node' is NULL. + +The *upscli_dump_authconf_list()* function returns the number of nodes +seen in the list (and likely printed). + +SEE ALSO +-------- + +linkman:upscli_create_authconf_item[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_get_authconf_list[3] diff --git a/docs/man/upscli_find_authconf_item.txt b/docs/man/upscli_find_authconf_item.txt new file mode 100644 index 0000000000..3d138b2813 --- /dev/null +++ b/docs/man/upscli_find_authconf_item.txt @@ -0,0 +1,122 @@ +UPSCLI_FIND_AUTHCONF_ITEM(3) +============================ + +NAME +---- + +upscli_find_authconf_item, upscli_get_authconf_item, +upscli_normalize_authconf_section_parts, +upscli_split_authconf_section - Find authentication configuration +items by their name components; help parse and normalize +name components for authentication configuration items + +SYNOPSIS +-------- + +------ + #include + + /* Item as it is on list, may be incomplete */ + upscli_authconf_t *upscli_find_authconf_item( + const char *user, + const char *host, + const char *port); + + /* Merge exact/host/global layers for a unique item */ + upscli_authconf_t *upscli_get_authconf_item( + const char *user, + const char *host, + const char *port, + int add_to_list); + + int upscli_normalize_authconf_section_parts( + char **out_normalized_sect_name, + char **p_sect_user, + int *out_fixed_sect_user, + char **p_sect_host, + char **p_sect_port); + + int upscli_split_authconf_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port); +------ + +DESCRIPTION +----------- + +The *upscli_find_authconf_item()* function searches the internal list of +authentication configurations for the best match for the given 'user', +'host', and 'port', and returns that list entry "as is" (the way it was +originally spelled in configuration files, adjusted just for section +title normalization). This function is primarily a stepping stone for +*upscli_get_authconf_item()* to do its work. + +The matching logic follows this priority: + +1. Exact match for `[user@host:port]` +2. Match for `[@host:port]` (host default) +3. Global default section (if 'user', 'host', and 'port' are all NULL, + or if there was no exact or host default match) + +The *upscli_get_authconf_item()* function goes a bit further and finds "parent" +entries (from exact match, to host defaults, to global defaults) to merge any +missing fields in that section to be inherited from the higher-level defaults. +If a specific `user` value was requested and only a non-exact match was found, +that fixed `USER=...` directive will be assumed and injected into the output. + +If `add_to_list` is '0', this function would return a new instance of the +`upscli_authconf_t` structure, which owns a separate copy of any strings +involved, and can be safely discarded with linkman:upscli_free_authconf_item[3] +(and MUST be discarded by caller, it is not added into the list). Each call +with same arguments returns a new instance, even with same data (or different, +if e.g. global defaults were changed for fields not populated in the original +item on the list). + +If `add_to_list` is '1', this function would return a new instance of the +structure and add it into the list, or would edit an existing item already +on the list by filling in the missing points inherited from higher levels. +On one hand, the caller does not have to manage freeing of this structure +and repetitive calls do not increase memory usage; on another, this precludes +adaptation to changing higher-level defaults (which is a reasonable approach +when configuration is loaded once). + +The "*upscli_split_authconf_section()*" function splits a `sect_name` which may +be from a user-typed configuration file into user, host and port sections, and +with "*upscli_normalize_authconf_section_parts()*" normalizes the values (e.g. a +`NULL` 'host' becomes `localhost`, a missing 'port' is defaulted to `NUT_PORT` +defined at build configuration time, e.g. '3493' by default, and a non-numeric +string 'port' is resolved in the naming database of the current operating +environment). The resulting normalized values are returned to caller using +pointers provided in the arguments (if not `NULL` in case of +"*upscli_split_authconf_section()*", must be not `NULL` in case of +"*upscli_normalize_authconf_section_parts()*"). + +A 'normalized_sect_name' can be also constructed and returned, so that varying +but ultimately identical definitions of the section titles (e.g. `[@localhost]` +and `[@localhost:3493]` can be conflated when parsing configuration files or +searching in the list. + +RETURN VALUE +------------ + +The *upscli_find_authconf_item()* function returns a pointer to a `upscli_authconf_t` +structure containing the matched configuration, or NULL if no match is found. +Note that the returned pointer refers to an item in the internal list managed +by *libupsclient*; it should not be freed directly by the caller unless they +are managing their own list. + +The *upscli_free_authconf_item()* function returns the last known value of the `next` +pointer field from the node being freed. + +SEE ALSO +-------- + +linkman:upscli_read_authconf_file[3], +linkman:upscli_get_authconf_list[3], +linkman:upscli_free_authconf_list[3], +linkman:upscli_clone_authconf_item[3], +linkman:upscli_merge_authconf_item[3], +linkman:upscli_free_authconf_item[3] diff --git a/docs/man/upscli_free_authconf_list.txt b/docs/man/upscli_free_authconf_list.txt new file mode 100644 index 0000000000..67525afa79 --- /dev/null +++ b/docs/man/upscli_free_authconf_list.txt @@ -0,0 +1,34 @@ +UPSCLI_FREE_AUTHCONF_LIST(3) +============================ + +NAME +---- + +upscli_free_authconf_list - Free the list of authentication configurations + +SYNOPSIS +-------- + +------ + #include + + void upscli_free_authconf_list(void); +------ + +DESCRIPTION +----------- + +The *upscli_free_authconf_list()* function frees the memory allocated for the +entire list and resets the internal list pointer to NULL. + +RETURN VALUE +------------ + +The *upscli_free_authconf_list()* function returns nothing. + +SEE ALSO +-------- + +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_get_authconf_list[3], linkman:upscli_dump_authconf_list[3], +linkman:upscli_create_authconf_item[3], linkman:upscli_free_authconf_item[3] diff --git a/docs/man/upscli_get_authconf_list.txt b/docs/man/upscli_get_authconf_list.txt new file mode 100644 index 0000000000..518fd0fcbb --- /dev/null +++ b/docs/man/upscli_get_authconf_list.txt @@ -0,0 +1,40 @@ +UPSCLI_GET_AUTHCONF_LIST(3) +=========================== + +NAME +---- + +upscli_get_authconf_list - Get the list of known authentication configurations + +SYNOPSIS +-------- + +------ + #include + + upscli_authconf_t *upscli_get_authconf_list(void); +------ + +DESCRIPTION +----------- + +The *upscli_get_authconf_list()* function returns a pointer to the internal list +of authentication configurations parsed from the configuration file (usually +*nutauth.conf*) via linkman:upscli_read_authconf_file[3]. + +Each element in the list is of type `upscli_authconf_t` as detailed in +linkman:upscli_create_authconf_item[3]. + +RETURN VALUE +------------ + +The *upscli_get_authconf_list()* function returns a pointer to the first element +of the `upscli_authconf_t` list, or NULL if the list is empty or hasn't been +initialized by linkman:upscli_read_authconf_file[3]. + +SEE ALSO +-------- + +linkman:upscli_create_authconf_item[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_free_authconf_list[3], linkman:upscli_dump_authconf_list[3] diff --git a/docs/man/upscli_init.txt b/docs/man/upscli_init.txt index 83aed550fa..5dff6f0573 100644 --- a/docs/man/upscli_init.txt +++ b/docs/man/upscli_init.txt @@ -4,7 +4,8 @@ UPSCLI_INIT(3) NAME ---- -upscli_init, upscli_init2 - Initialize upsclient module specifying security properties. +upscli_init, upscli_init2, upscli_init_authconf - Initialize upsclient module +specifying security properties. SYNOPSIS -------- @@ -26,6 +27,8 @@ SYNOPSIS const char *certname, const char *certpasswd, const char *certfile); + + int upscli_init_authconf(upscli_authconf_t *ac); ------ DESCRIPTION @@ -86,6 +89,10 @@ In both cases, the 'certname' (if not empty) can be used to verify that the specified file provides a certificate with expected subject name, or possibly matches the expected host name or IP address. +The *upscli_init_authconf()* function uses the `upscli_authconf_t` structure +populated by linkman:upscli_read_authconf_file[3] to pass equivalent information +from linkman:nutauth.conf[5] file(s). + Other nuances ------------- diff --git a/docs/man/upscli_read_authconf_file.txt b/docs/man/upscli_read_authconf_file.txt new file mode 100644 index 0000000000..7b37e5d0b9 --- /dev/null +++ b/docs/man/upscli_read_authconf_file.txt @@ -0,0 +1,67 @@ +UPSCLI_READ_AUTHCONF_FILE(3) +============================ + +NAME +---- + +upscli_read_authconf_file - Read the authentication configuration file + +SYNOPSIS +-------- + +------ + #include + + int upscli_read_authconf_file(const char *filename, int fatal_errors); +------ + +DESCRIPTION +----------- + +The *upscli_read_authconf_file()* function reads the specified 'filename' +(which is usually the path to *nutauth.conf*) and populates an internal +list of authentication and SSL configurations. + +If 'filename' is `NULL`, the function first tries to locate either a file +whose path and name is fully provided in `${NUT_AUTHCONF_FILE}` environment +variable, or a `${NUT_AUTHCONF_PATH}/nutauth.conf`, and would not try any +other locations IFF these environment variables are provided. Otherwise it +tries per-user `${HOME}/.config/nut/nutauth.conf` or `${HOME}/.nutauth.conf`, +or a site default `${NUT_CONFDIR}/nutauth.conf` (whichever is found first). +Such a file may `INCLUDE` further configurations (e.g. hop from a per-user +file to load server-wide defaults), if desired. + +The file structure is similar to *ups.conf*, with global defaults and +per-server sections named like `[@localhost:12345]` for host defaults, +or `[username@localhost:12345]` for specific account overrides. + +If 'fatal_errors' is non-zero, the function may call abort the program on +critical failures (like memory allocation errors or if the file cannot +be opened). + +See the linkman:nutauth.conf[5] manual page for supported configuration +keywords. + +NESTING (INCLUDE FILES) +~~~~~~~~~~~~~~~~~~~~~~~ + +Included files are supported via the `INCLUDE` directive for optionally +present files, and `INCLUDE_REQUIRED` for files that must be there +(otherwise the program exits with a fatal error). + +Global-scope includes may modify global default items and define new sections. + +Section-scope includes (nested within a section) can only modify data +within that section. + +RETURN VALUE +------------ + +The *upscli_read_authconf_file()* function returns '1' on success, or '-1' if an +error occurs (and 'fatal_errors' was zero). + +SEE ALSO +-------- + +linkman:upscli_get_authconf_list[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_dump_authconf_list[3], linkman:nutauth.conf[5] diff --git a/docs/man/upscmd.txt b/docs/man/upscmd.txt index d8793248e0..9bfc10b597 100644 --- a/docs/man/upscmd.txt +++ b/docs/man/upscmd.txt @@ -80,6 +80,27 @@ Set the timeout for initial network connections (by default they are indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + UNATTENDED MODE --------------- @@ -132,7 +153,8 @@ It involves magic cookies. SEE ALSO -------- -linkman:upsd[8], linkman:upsrw[8] +linkman:upsd[8], linkman:upsrw[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsimage.cgi.txt b/docs/man/upsimage.cgi.txt index dc38aeb243..e0b762c880 100644 --- a/docs/man/upsimage.cgi.txt +++ b/docs/man/upsimage.cgi.txt @@ -35,6 +35,8 @@ upsstats will only talk to linkman:upsd[8] servers that have been defined in your linkman:hosts.conf[5]. If it complains about "Access to that host is not authorized", check that file first. +SSL access may be further managed by linkman:nutauth.conf[5] file. + FILES ----- @@ -43,7 +45,8 @@ linkman:hosts.conf[5] SEE ALSO -------- -linkman:upsd[8], linkman:upsstats.cgi[8] +linkman:upsd[8], linkman:upsstats.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upslog.txt b/docs/man/upslog.txt index a1499134c9..840970bcee 100644 --- a/docs/man/upslog.txt +++ b/docs/man/upslog.txt @@ -155,6 +155,27 @@ Set the timeout for initial network connections (by default they are indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + SERVICE DELAYS -------------- @@ -204,6 +225,11 @@ Since NUT v2.8.3, the single-UPS options are added to the list of tuples, so both legacy and new options can be reliably used to monitor multiple devices in the same run. +Since this client can establish multiple connections, keep in mind that +currently it can only identify itself with some one (first seen) client +certificate, if `CERTIDENT` settings are used in the linkman:nutauth.conf[5] +file. Multiple `CERTHOST` directives for specially trusted servers can be used. + SEE ALSO -------- @@ -216,7 +242,8 @@ Clients: ~~~~~~~~ linkman:upsc[8], linkman:upscmd[8], -linkman:upsrw[8], linkman:upsmon[8], linkman:upssched[8] +linkman:upsrw[8], linkman:upsmon[8], linkman:upssched[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsmon.conf.txt b/docs/man/upsmon.conf.txt index 83fcf4df0d..a6e206db87 100644 --- a/docs/man/upsmon.conf.txt +++ b/docs/man/upsmon.conf.txt @@ -640,6 +640,13 @@ the connection must be "secured" (should there be no fallback to plain-text?) Up until NUT release v2.8.5 this was a no-op when built with OpenSSL. Now this feature is supported with both OpenSSL >= 1.1.0 and NSS backends. + +For a data server with non-default port, it should be specified as a +colon-separated part of 'hostname', e.g. ++ +------ +CERTHOST "localhost:34930" "My nut server" 1 1 +------ ++ NOTE: Be sure to enclose "certificate name" in double-quotes if you are using a value with spaces in it. diff --git a/docs/man/upsrw.txt b/docs/man/upsrw.txt index fdb4891cfd..4578b09037 100644 --- a/docs/man/upsrw.txt +++ b/docs/man/upsrw.txt @@ -97,6 +97,27 @@ Set the timeout for initial network connections (by default they are indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + UNATTENDED MODE --------------- @@ -144,7 +165,8 @@ confusing. SEE ALSO -------- -linkman:upsd[8], linkman:upscmd[8] +linkman:upsd[8], linkman:upscmd[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsset.cgi.txt b/docs/man/upsset.cgi.txt index bb88d81b9b..9380ee7b31 100644 --- a/docs/man/upsset.cgi.txt +++ b/docs/man/upsset.cgi.txt @@ -82,6 +82,8 @@ upsset will only talk to linkman:upsd[8] servers that have been defined in your linkman:hosts.conf[5]. If it complains about "Access to that host is not authorized", check your hosts.conf first. +SSL access may be further managed by linkman:nutauth.conf[5] file. + SECURITY -------- @@ -95,7 +97,8 @@ The short explanation is--if you can't lock it down, don't try to run it. FILES ----- -linkman:hosts.conf[5], linkman:upsset.conf[5] +linkman:hosts.conf[5], linkman:upsset.conf[5], +linkman:nutauth.conf[5] SEE ALSO -------- diff --git a/docs/man/upsset.conf.txt b/docs/man/upsset.conf.txt index b617bb0df3..bf9c3200f1 100644 --- a/docs/man/upsset.conf.txt +++ b/docs/man/upsset.conf.txt @@ -196,7 +196,8 @@ web server, don't blame me. SEE ALSO -------- -linkman:upsset.cgi[8] +linkman:upsset.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index b30a37a03e..6d76a1ee1a 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -41,6 +41,8 @@ upsstats will only talk to linkman:upsd[8] servers that have been defined in your linkman:hosts.conf[5]. If it complains that "Access to that host is not authorized", check that file first. +SSL access may be further managed by linkman:nutauth.conf[5] file. + TEMPLATES --------- @@ -114,7 +116,8 @@ linkman:hosts.conf[5], linkman:upsstats.html[5], upsstats-single.html SEE ALSO -------- -linkman:upsimage.cgi[8] +linkman:upsimage.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/nut.dict b/docs/nut.dict index a8b49e7432..6a13bf5c0a 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3749 utf-8 +personal_ws-1.1 en 3754 utf-8 AAC AAS ABI @@ -1240,6 +1240,7 @@ SPS SRC SRVS SSD +SSLBACKEND SSLContext SSSS STARTTLS @@ -1690,6 +1691,7 @@ authPassword authPriv authProtocol authType +authconf autoboot autoconf autodetect @@ -2195,6 +2197,7 @@ formatconfig formatstring fosshost fp +fprintf freebsd freedesktop freeipmi @@ -2837,6 +2840,7 @@ numa numbatteries numlogins numq +nutauth nutclient nutclientmem nutconf @@ -3284,6 +3288,7 @@ spectype spellcheck spellchecked splitaddr +splitauth splitname sprintf squasher diff --git a/drivers/libusb0.c b/drivers/libusb0.c index ab002f36f3..52f08f2a98 100644 --- a/drivers/libusb0.c +++ b/drivers/libusb0.c @@ -43,8 +43,9 @@ #include "usb-common.h" #include "nut_libusb.h" #ifdef WIN32 -#include "wincompat.h" +# include "wincompat.h" #endif /* WIN32 */ +#include "strcasestr-static.h" #define USB_DRIVER_NAME "USB communication driver (libusb 0.1)" #define USB_DRIVER_VERSION "0.53" @@ -60,14 +61,6 @@ upsdrv_info_t comm_upsdrv_info = { #define MAX_REPORT_SIZE 0x1800 -#if (!HAVE_STRCASESTR) && (HAVE_STRSTR && HAVE_STRLWR && HAVE_STRDUP) -/* Only used in this file of all NUT codebase, so not in str.{c,h} - * where it happens to conflict with netsnmp-provided variant for - * some of our build products. - */ -static char *strcasestr(const char *haystack, const char *needle); -#endif - static void nut_libusb_close(usb_dev_handle *udev); /*! Add USB-related driver variables with addvar() and dstate_setinfo(). @@ -1015,37 +1008,6 @@ static void nut_libusb_close(usb_dev_handle *udev) usb_close(udev); } -#if (!HAVE_STRCASESTR) && (HAVE_STRSTR && HAVE_STRLWR && HAVE_STRDUP) -static char *strcasestr(const char *haystack, const char *needle) { - /* work around "const char *" and guarantee the original is not - * touched... not efficient but we have few uses for this method */ - char * dH = NULL, *dN = NULL, *lH = NULL, *lN = NULL, *first = NULL; - - dH = strdup(haystack); - if (dH == NULL) goto err; - dN = strdup(needle); - if (dN == NULL) goto err; - lH = strlwr(dH); - if (lH == NULL) goto err; - lN = strlwr(dN); - if (lN == NULL) goto err; - first = strstr(lH, lN); - -err: - if (dH != NULL) free(dH); - if (dN != NULL) free(dN); - /* Does this implementation of strlwr() change original buffer? */ - if (lH != dH && lH != NULL) free(lH); - if (lN != dN && lN != NULL) free(lN); - if (first == NULL) { - return NULL; - } - - /* Pointer to first char of the needle found in original haystack */ - return (char *)(haystack + (first - lH)); -} -#endif - usb_communication_subdriver_t usb_subdriver = { USB_DRIVER_NAME, USB_DRIVER_VERSION, diff --git a/include/Makefile.am b/include/Makefile.am index 8acb2da620..c2bb6f8820 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -14,7 +14,7 @@ dist_noinst_HEADERS = \ attribute.h common.h extstate.h proto.h \ state.h str.h strjson.h timehead.h upsconf.h \ nut_bool.h nut_float.h nut_stdint.h nut_platform.h \ - wincompat.h + strcasestr-static.h wincompat.h # Optionally deliverable as part of NUT public API: if WITH_DEV diff --git a/include/common.h b/include/common.h index d493ac7565..e4021e0164 100644 --- a/include/common.h +++ b/include/common.h @@ -423,6 +423,9 @@ int str_add_unique_token( /* Report maximum platform value for the pid_t */ pid_t get_max_pid_t(void); +/* Check filesystem permissions for files/dirs we deem secretive */ +void check_perms(const char *fn); + /* send sig to pid after some sanity checks, returns * -1 for error, or zero for a successfully sent signal */ int sendsignalpid(pid_t pid, int sig, const char *progname, int check_current_progname); diff --git a/include/strcasestr-static.h b/include/strcasestr-static.h new file mode 100644 index 0000000000..bac5ab7a5d --- /dev/null +++ b/include/strcasestr-static.h @@ -0,0 +1,73 @@ +/*! + * @file strcasestr-static.h + * @brief Fallback implementation of strcasestr() as a static method included + * into a few sources on a need-to-know basis + * + * @author Copyright (C) + * 2022 - 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * -------------------------------------------------------------------------- */ + +#ifndef NUT_STRCASESTR_STATIC_H_SEEN +#define NUT_STRCASESTR_STATIC_H_SEEN 1 + +#include "config.h" /* Did configure script discover what we miss and need? */ + +# if (!(defined(HAVE_STRCASESTR) && HAVE_STRCASESTR)) && (HAVE_STRSTR && HAVE_STRLWR && HAVE_STRDUP) +/** Only used in a few file of all NUT codebase (libusb0.c, upsclient.c), + * so not published in str.{c,h} where it happens to conflict with an + * optional netsnmp-provided variant for some of our build products. + * Here it is just included into the few "victims". + */ +static char *strcasestr(const char *haystack, const char *needle) +{ + /* work around "const char *" and guarantee the original is not + * touched... not efficient but we have few uses for this method */ + char * dH = NULL, *dN = NULL, *lH = NULL, *lN = NULL, *first = NULL; + + dH = strdup(haystack); + if (dH == NULL) goto err; + dN = strdup(needle); + if (dN == NULL) goto err; + lH = strlwr(dH); + if (lH == NULL) goto err; + lN = strlwr(dN); + if (lN == NULL) goto err; + first = strstr(lH, lN); + +err: + if (dH != NULL) free(dH); + if (dN != NULL) free(dN); + /* Does this implementation of strlwr() change original buffer? */ + if (lH != dH && lH != NULL) free(lH); + if (lN != dN && lN != NULL) free(lN); + if (first == NULL) { + return NULL; + } + + /* Pointer to first char of the needle found in original haystack */ + return (char *)(haystack + (first - lH)); +} + +# ifdef HAVE_STRCASESTR +# undef HAVE_STRCASESTR +# endif +# define HAVE_STRCASESTR 1 + +# endif /* (!HAVE_STRCASESTR) && (HAVE_STRSTR && HAVE_STRLWR && HAVE_STRDUP) */ + +#endif /* NUT_STRCASESTR_STATIC_H_SEEN */ diff --git a/lib/libnutclient.pc.in b/lib/libnutclient.pc.in index d4629efbe3..9bfc668b3f 100644 --- a/lib/libnutclient.pc.in +++ b/lib/libnutclient.pc.in @@ -11,3 +11,4 @@ Description: UPS monitoring with Network UPS Tools Version: @PACKAGE_VERSION@ Libs: -L${libdir} @LDFLAGS_NUT_RPATH_CXX@ -lnutclient Cflags: -I${includedir} +Requires: @LIBSSL_REQUIRES@ diff --git a/lib/libupsclient-config.in b/lib/libupsclient-config.in index 089c8bf8dd..c55dcbfe47 100644 --- a/lib/libupsclient-config.in +++ b/lib/libupsclient-config.in @@ -21,7 +21,7 @@ libexecdir="@libexecdir@" libdir="@libdir@" includedir="@includedir@" confdir="@CONFPATH@" -Libs="-L@libdir@ @LDFLAGS_NUT_RPATH@ -lupsclient @LIBSSL_LIBS@" +Libs="-L@libdir@ @LDFLAGS_NUT_RPATH@ -lupsclient @LIBSSL_LDFLAGS_RPATH@ @LIBSSL_LIBS@" Cflags="-I@includedir@ @LIBSSL_CFLAGS@" ConfigFlags='@CONFIG_FLAGS@' diff --git a/scripts/obs/debian.nut-client.install b/scripts/obs/debian.nut-client.install index b899f84d60..b5356e1063 100644 --- a/scripts/obs/debian.nut-client.install +++ b/scripts/obs/debian.nut-client.install @@ -6,6 +6,7 @@ debian/tmp/sbin/upsmon debian/tmp/sbin/upssched debian/tmp/bin/upssched-cmd debian/tmp/etc/nut/nut.conf +debian/tmp/etc/nut/nutauth.conf debian/tmp/etc/nut/upsmon.conf debian/tmp/etc/nut/upssched.conf debian/tmp/usr/share/augeas/lenses/dist/nuthostsconf.aug diff --git a/scripts/obs/debian.nut-client.manpages b/scripts/obs/debian.nut-client.manpages index 115e821706..94744f65ce 100644 --- a/scripts/obs/debian.nut-client.manpages +++ b/scripts/obs/debian.nut-client.manpages @@ -4,6 +4,7 @@ debian/tmp/usr/share/man/man8/upsmon.8 debian/tmp/usr/share/man/man8/upsrw.8 debian/tmp/usr/share/man/man8/upssched.8 debian/tmp/usr/share/man/man5/nut.conf.5 +debian/tmp/usr/share/man/man5/nutauth.conf.5 debian/tmp/usr/share/man/man5/upsmon.conf.5 debian/tmp/usr/share/man/man5/upssched.conf.5 debian/tmp/usr/share/man/man8/upslog.8 diff --git a/scripts/obs/nut.spec b/scripts/obs/nut.spec index 158aca99c7..72fbb3c0d4 100644 --- a/scripts/obs/nut.spec +++ b/scripts/obs/nut.spec @@ -573,8 +573,8 @@ bin/chown -R %{NUT_USER} %{STATEPATH} || echo "WARNING: Could not secure state p bin/chgrp -R %{NUT_GROUP} %{STATEPATH} || echo "WARNING: Could not secure state path '%{STATEPATH}'" >&2 # Be sure that all files are owned by a dedicated user. bin/chown %{NUT_USER} %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 -bin/chgrp root %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 -bin/chmod 600 %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 +bin/chgrp root %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users %{CONFPATH}/nutauth.conf || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 +bin/chmod 600 %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users %{CONFPATH}/nutauth.conf || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 # And finally trigger udev to set permissions according to newly installed rules files. if [ -x /sbin/udevadm ] ; then /sbin/udevadm trigger --subsystem-match=usb --property-match=DEVTYPE=usb_device ; fi %if "x%{?systemdtmpfilesdir}" == "x" @@ -691,6 +691,7 @@ if [ -x /sbin/udevadm ] ; then /sbin/udevadm trigger --subsystem-match=usb --pro ### FIXME: if under /etc ### % config(noreplace) % {UDEVRULEPATH}/rules.d/*.rules %{UDEVRULEPATH}/rules.d/*.rules %config(noreplace) %{CONFPATH}/hosts.conf +%config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/nutauth.conf %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsd.conf %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsd.users %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsmon.conf diff --git a/server/upsd.c b/server/upsd.c index 26782d2d8f..54d9fcbf2e 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -2492,28 +2492,6 @@ static void setup_signals(void) #endif /* WIN32 */ } -void check_perms(const char *fn) -{ -#ifndef WIN32 - int ret; - struct stat st; - - ret = stat(fn, &st); - - if (ret != 0) { - fatal_with_errno(EXIT_FAILURE, "stat %s", fn); - } - - /* include the x bit here in case we check a directory */ - if (st.st_mode & (S_IROTH | S_IXOTH)) { - upslogx(LOG_WARNING, "WARNING: %s is world readable (hope you don't have passwords there)", fn); - } -#else /* WIN32 */ - NUT_UNUSED_VARIABLE(fn); - NUT_WIN32_INCOMPLETE_MAYBE_NOT_APPLICABLE(); -#endif /* WIN32 */ -} - void close_oldest_client(void) { nut_ctype_t *client, *oldest = NULL; diff --git a/server/upsd.h b/server/upsd.h index 1fe5c02284..35007ac0fa 100644 --- a/server/upsd.h +++ b/server/upsd.h @@ -99,8 +99,6 @@ void close_oldest_client(void); */ #define RESERVE_FD_COUNT_UPSD 8 -void check_perms(const char *fn); - /* return values for instcmd / setvar status tracking, * mapped on drivers/upshandler.h, apart from STAT_PENDING (initial state) */ enum { diff --git a/server/user.h b/server/user.h index 613a0c5eef..c20eefef16 100644 --- a/server/user.h +++ b/server/user.h @@ -38,9 +38,6 @@ int user_checkaction(const char *un, const char *pw, const char *action); void user_flush(void); -/* cheat - we don't want the full upsd.h included here */ -void check_perms(const char *fn); - #ifdef __cplusplus /* *INDENT-OFF* */ } diff --git a/tests/Makefile.am b/tests/Makefile.am index 2bdab40017..bfda0cff97 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -47,6 +47,7 @@ $(top_builddir)/common/libnutconf.la \ $(top_builddir)/common/libcommonclient.la \ $(top_builddir)/common/libcommon.la \ $(top_builddir)/common/libparseconf.la \ +$(top_builddir)/clients/libupsclient.la \ $(top_builddir)/clients/libnutclient.la \ $(top_builddir)/clients/libnutclientstub.la: dummy @dotMAKE@ +@cd $(@D) && $(MAKE) $(AM_MAKEFLAGS) $(@F) @@ -72,6 +73,7 @@ $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-comm $(top_builddir)/drivers/libdummy_mockdrv.la: $(top_builddir)/common/libcommonversion.la $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la $(top_builddir)/common/libnutconf.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la $(top_builddir)/clients/libnutclient.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la +$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-client.la $(top_builddir)/include/nut_version.h NUT_LIBCOMMON = $(top_builddir)/common/libnutprivate-@NUT_SOURCE_GITREV_SEMVER_UNDERSCORES@-common-all.la @@ -80,6 +82,7 @@ else !ENABLE_SHARED_PRIVATE_LIBS $(top_builddir)/drivers/libdummy_mockdrv.la: $(top_builddir)/common/libcommon.la $(top_builddir)/common/libcommonversion.la $(top_builddir)/common/libparseconf.la $(top_builddir)/common/libnutconf.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/clients/libnutclient.la: $(top_builddir)/common/libcommonclient.la +$(top_builddir)/clients/libupsclient.la: $(top_builddir)/common/libcommonclient.la $(top_builddir)/include/nut_version.h NUT_LIBCOMMON = $(top_builddir)/common/libcommon.la @@ -114,6 +117,18 @@ TESTS += nutbooltest nutbooltest_SOURCES = nutbooltest.c #nutbooltest_LDADD = $(NUT_LIBCOMMON) +TESTS += test_authconf +test_authconf_SOURCES = test_authconf.c +test_authconf_LDADD = $(top_builddir)/clients/libupsclient.la $(NUT_LIBCOMMON) +test_authconf_LDFLAGS = $(AM_LDFLAGS) +test_authconf_CFLAGS = $(AM_CFLAGS) -I$(top_srcdir)/clients +if WITH_SSL +# This is more for libupsclient that may be linked to NSS than for the test itself +test_authconf_LDADD += $(LIBSSL_LIBS) +test_authconf_LDFLAGS += $(LIBSSL_LDFLAGS_RPATH) +test_authconf_CFLAGS += $(LIBSSL_CFLAGS) +endif WITH_SSL + # Separate the .deps of other dirs from this one LINKED_SOURCE_FILES = hidparser.c diff --git a/tests/NIT/Makefile.am b/tests/NIT/Makefile.am index fb4c7f2eed..2836dc1bd9 100644 --- a/tests/NIT/Makefile.am +++ b/tests/NIT/Makefile.am @@ -58,10 +58,12 @@ check-NIT: $(abs_srcdir)/nit.sh # Make sure pre-requisites for NIT are fresh as we iterate check-NIT-devel: $(abs_srcdir)/nit.sh @dotMAKE@ + +@cd "$(top_builddir)" && $(MAKE) $(AM_MAKEFLAGS) -s generated-sources-with-a-touch +@cd .. && ( $(MAKE) $(AM_MAKEFLAGS) -s cppnit$(EXEEXT) || echo "OPTIONAL C++ test client test will be skipped" ) +@cd "$(top_builddir)/clients" && $(MAKE) $(AM_MAKEFLAGS) -s upsc$(EXEEXT) upscmd$(EXEEXT) upsrw$(EXEEXT) upsmon$(EXEEXT) +@cd "$(top_builddir)/server" && $(MAKE) $(AM_MAKEFLAGS) -s upsd$(EXEEXT) sockdebug$(EXEEXT) +@cd "$(top_builddir)/drivers" && $(MAKE) $(AM_MAKEFLAGS) -s dummy-ups$(EXEEXT) upsdrvctl$(EXEEXT) + +@cd "$(top_builddir)" && $(MAKE) $(AM_MAKEFLAGS) -s cleanup-touchfiles-for-generated-headers +@$(MAKE) $(AM_MAKEFLAGS) check-NIT # Allow to override with make/env vars; provide sensible defaults (see nit.sh): diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index da1d55fb11..16aa818ea1 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -841,7 +841,11 @@ NUT_STATEPATH="${TESTDIR}/run" NUT_PIDPATH="${TESTDIR}/run" NUT_ALTPIDPATH="${TESTDIR}/run" NUT_CONFPATH="${TESTDIR}/etc" -export NUT_STATEPATH NUT_PIDPATH NUT_ALTPIDPATH NUT_CONFPATH +# Leave no ambiguity as to which nutauth.conf should be read by default: +# "${NUT_CONFPATH}/nutauth.conf" (e.g. not fall back to user or site configs). +# Only apply it after (re-)generating via generatecfg_nutauth() though: +NUT_AUTHCONF_FILE="none" +export NUT_STATEPATH NUT_PIDPATH NUT_ALTPIDPATH NUT_CONFPATH NUT_AUTHCONF_FILE if [ -f "${NUT_CONFPATH}/NIT.env-sandbox-ready" ] ; then log_warn "'${NUT_CONFPATH}/NIT.env-sandbox-ready' exists, do you have another instance of the script still running?" @@ -1431,7 +1435,7 @@ EOF # Import the CA certificate, so users of this DB trust it: certutil -A -d . -f .pwfile \ -n "${TESTCERT_ROOTCA_NAME}" \ - -t "TC,," \ + -t "CT,C,C" \ -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ || die "Could not import the CA certificate to NSS Server database" @@ -1441,8 +1445,8 @@ EOF -s "CN=${TESTCERT_SERVER_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" \ -a -o server.req \ -z "${TESTCERT_PATH_ROOTCA}"/.random \ - --extKeyUsage "serverAuth" \ - --nsCertType sslServer \ + --extKeyUsage "serverAuth,critical" \ + --nsCertType sslServer,critical \ --keyUsage critical,dataEncipherment,keyEncipherment,digitalSignature,nonRepudiation \ --extSAN "dns:localhost,dns:localhost6,dns:nut-server-$$.localdomain,dns:127.0.0.1,dns:::1,ip:127.0.0.1,ip:::1,ip:127.1.2.`expr $$ % 200`" \ || die "Could not create a NSS Server certificate request" @@ -1456,12 +1460,13 @@ EOF -f "${TESTCERT_PATH_ROOTCA}"/.pwfile \ -c "${TESTCERT_ROOTCA_NAME}" \ -a -i server.req -o server.crt \ - --extKeyUsage "serverAuth" \ - --nsCertType sslServer \ + --extKeyUsage "serverAuth,critical" \ + --nsCertType sslServer,critical \ -m 2 \ -2 \ -3 \ -v "${TESTCERT_VALIDITY_MONTHS}" \ + -t "u,u,u" \ --extSKID } if [ x"${NUT_CERTUTIL_INTERACTIVE-}" = xtrue ] ; then @@ -1499,7 +1504,7 @@ EOF # Import the signed certificate into server database: certutil -A -d . -f .pwfile \ -n "${TESTCERT_SERVER_NAME}" \ - -a -i server.crt -t ",," \ + -a -i server.crt -t "u,u,u" \ || die "Could not import the signed NSS Server certificate into server database" if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] \ @@ -1609,7 +1614,7 @@ EOF # Import the CA certificate, so users of this DB trust it: certutil -A -d . -f .pwfile \ -n "${TESTCERT_ROOTCA_NAME}" \ - -t "TC,," \ + -t "CT,C,C" \ -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ || die "Could not import the CA certificate to NSS Server database" @@ -1928,7 +1933,7 @@ set | ${EGREP} '^(NUT_|TESTDIR|TESTCERT|LD_LIBRARY_PATH|DEBUG|PATH).*=' \ LD_LIBRARY_PATH_CLIENT|LD_LIBRARY_PATH_ORIG|PATH_*|NUT_PORT_*|TESTDIR_*) continue ;; - DEBUG_SLEEP|PATH|LD_LIBRARY_PATH*) printf '### ' ;; + DEBUG_SLEEP|PATH|LD_LIBRARY_PATH*|NUT_AUTHCONF_FILE) printf '### ' ;; esac case "$V" in @@ -2068,6 +2073,7 @@ EOF ### upsd.users: ################################################## TESTPASS_ADMIN='mypass' +TESTPASS_READER='public' TESTPASS_TESTER='pass words' TESTPASS_UPSMON_PRIMARY='P@ssW0rdAdm' TESTPASS_UPSMON_SECONDARY='P@ssW0rd' @@ -2079,6 +2085,10 @@ generatecfg_upsdusers_trivial() { actions = SET instcmds = ALL +[reader] + password = $TESTPASS_READER + # No actions nor instcmds allowed + [tester] password = "${TESTPASS_TESTER}" instcmds = test.battery.start @@ -2294,19 +2304,19 @@ EOF x"none") cat << EOF CERTVERIFY 0 # Custom settings for a specific remote server: -CERTHOST localhost "${TESTCERT_SERVER_NAME}" 1 0 +CERTHOST "localhost:${NUT_PORT}" "${TESTCERT_SERVER_NAME}" 1 0 EOF ;; x"addr") cat << EOF CERTVERIFY 1 # Custom settings for a specific remote server without verifying the host cert for nickname '${TESTCERT_SERVER_NAME}': -CERTHOST localhost "" 1 1 +CERTHOST "localhost:${NUT_PORT}" "" 1 1 EOF ;; *) cat << EOF CERTVERIFY 1 # Custom settings for a specific remote server: -CERTHOST localhost "${TESTCERT_SERVER_NAME}" 1 1 +CERTHOST "localhost:${NUT_PORT}" "${TESTCERT_SERVER_NAME}" 1 1 EOF ;; esac @@ -2318,6 +2328,163 @@ EOF export NUT_QUIET_INIT_SSL } +### nutauth.conf: ############################################# + +generatecfg_nutauth() { + # NOTE: Tools will by default read from whatever "${NUT_AUTHCONF_FILE}" + # resolves to, but here we populate the tests' instance (not overwrite + # some user configuration file, if that is somehow supplied)! + { cat << EOF +# Global section for nutauth.conf, inherited and overridden per line by others +EOF + + case "${WITH_SSL_CLIENT}" in + none) ;; + OpenSSL) + log_info "Adding ${WITH_SSL_CLIENT} client-side SSL config to nutauth.conf" + cat << EOF +# OpenSSL CERTFILE: PEM file with client cert, possibly the +# intermediate and root CA's, and finally corresponding private key +CERTFILE = "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}upsmon.pem" + +# OpenSSL CERTPATH: Directory with PEM file(s), looked up by the +# CA subject name hash value (which must include our NUT server). +# Here we just use the path for PEM file that should be populated +# by the generatecfg_upsd_add_SSL() method. +CERTPATH = "${TESTCERT_PATH_ROOTCA}" +EOF + ;; + NSS) + log_info "Adding ${WITH_SSL_CLIENT} client-side SSL config to nutauth.conf" + cat << EOF +# NSS CERTPATH: Directory with 3-file database of cert/key store +CERTPATH = "${TESTCERT_PATH_CLIENT}" +EOF + ;; + esac + + # Shared features for both SSL backends: + case x"${WITH_SSL_CLIENT_CERTIDENT}" in + x"name+pass") + cat << EOF +# SSL enabled, our cert nickname and private key password: Who am I? +CERTIDENT_NAME = "${TESTCERT_CLIENT_NAME}" +CERTIDENT_PASS = "${TESTCERT_CLIENT_PASS}" +EOF + ;; + x"name") # Really unlikely + cat << EOF +# SSL enabled, our cert nickname and private key password: Who am I? +CERTIDENT_NAME = "${TESTCERT_CLIENT_NAME}" +# A really unlikely case: this backend does not support passphrases?.. +CERTIDENT_PASS = "" +EOF + ;; + x"pass") + cat << EOF +# SSL enabled, our cert nickname and private key password: Who am I? +# This SSL backend can not check cert subject... +CERTIDENT_NAME = "" +# ...but at least can do private key passwords: +CERTIDENT_PASS = "${TESTCERT_CLIENT_PASS}" +EOF + ;; + esac + + case "${WITH_SSL_CLIENT}" in + none) + cat << EOF +# SSL not enabled: do not check server certs, do not require STARTTLS success: +CERTVERIFY = 0 +FORCESSL = 0 + +[@localhost:${NUT_PORT}] +EOF + ;; + OpenSSL|NSS) + cat << EOF +# Defaults that CERTHOST may override per-server, but note +# that this impacts also the general NUT client behavior. +# 0 for OK to fail => proceed in plaintext (should be overridden +# by the specific localhost definition below): +FORCESSL = 0 + +# -1 for inheriting a better value elsewhere, e.g. in host +# definition below, or effectively 0 if never defined exactly: +CERTVERIFY = -1 + +[@localhost:${NUT_PORT}] # We also try different indentation and comment styles here +EOF + + if [ x"${WITH_SSL_SERVER}" != xnone ] ; then + case x"${WITH_SSL_CLIENT_CERTHOST}" in + x"none") cat << EOF + # Custom settings for a specific remote server: + CERTHOST = "${TESTCERT_SERVER_NAME}" +CERTVERIFY = 1 + FORCESSL = 0 +EOF + ;; + x"addr") cat << EOF + # Custom settings for a specific remote server without verifying + # the host cert for nickname '${TESTCERT_SERVER_NAME}': + # CERTHOST = "" +# Just verify the CA matches what we trust: +CERTVERIFY = 1 + FORCESSL = 1 +EOF + ;; + *) cat << EOF +# Custom settings for a specific remote server: +CERTHOST = "${TESTCERT_SERVER_NAME}" +CERTVERIFY = 1 + FORCESSL = 1 +EOF + ;; + esac + fi + ;; + esac + + # Previous clauses end somewhere in the [@localhost:${NUT_PORT}] section + # Keep credentials in sync with generatecfg_upsdusers_trivial() + cat << EOF + # Default credentials for access to this server + USERNAME = reader + PASS = "$TESTPASS_READER" + +[admin@:${NUT_PORT}] + # Empty host should resolve to "localhost" + # Unquoted password, no special characters here: + PASS = $TESTPASS_ADMIN + +[tester@localhost:${NUT_PORT}] + password = "${TESTPASS_TESTER}" + +[dummy-admin-m@localhost:${NUT_PORT}] + pass = "${TESTPASS_UPSMON_PRIMARY}" + +[dummy-admin@localhost:${NUT_PORT}] + PASSWORD = "${TESTPASS_UPSMON_PRIMARY}" + +[dummy-user-s@localhost:${NUT_PORT}] +password = "${TESTPASS_UPSMON_SECONDARY}" + +[dummy-user@localhost:${NUT_PORT}] + password = "${TESTPASS_UPSMON_SECONDARY}" +EOF + + } > "${NUT_CONFPATH}/nutauth.conf" \ + && chmod 640 "${NUT_CONFPATH}/nutauth.conf" \ + || die "Failed to populate temporary FS structure for the NIT: nutauth.conf" + + NUT_QUIET_INIT_SSL=false + export NUT_QUIET_INIT_SSL + + NUT_AUTHCONF_FILE="${NUT_CONFPATH}/nutauth.conf" + export NUT_AUTHCONF_FILE +} + ### ups.conf: ################################################## generatecfg_ups_trivial() { @@ -2601,6 +2768,7 @@ testcase_upsd_allow_no_device() { generatecfg_upsd_nodev generatecfg_upsdusers_trivial generatecfg_ups_trivial + WITH_SSL_CLIENT=none WITH_SSL_SERVER=none generatecfg_nutauth if shouldDebug ; then ls -la "$NUT_CONFPATH/" || true fi @@ -2707,6 +2875,7 @@ generatecfg_sandbox() { generatecfg_upsd_nodev generatecfg_upsd_add_SSL generatecfg_upsdusers_trivial + generatecfg_nutauth generatecfg_ups_dummy } diff --git a/tests/test_authconf.c b/tests/test_authconf.c new file mode 100644 index 0000000000..88c78c9e65 --- /dev/null +++ b/tests/test_authconf.c @@ -0,0 +1,336 @@ +/* test_authconf.c - test program for client/authconf.c + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" + +#include "common.h" +#include "authconf.h" + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + const char *test_conf = "test_nutauth.conf"; + const char *include_conf = "test_include.conf"; + FILE *f; + upscli_authconf_t *ac, *ac5, *ac7, *ac8, *ac9, *ac12; + size_t num_sections; + char buf[512], *s; + int l, testnum = 0; + + s = getenv("NUT_DEBUG_LEVEL"); + if (s && str_to_int(s, &l, 10) && l > 0) { + nut_debug_level = l; + upsdebugx(1, "Defaulting debug verbosity to NUT_DEBUG_LEVEL=%d " + "since none was requested by command-line options", l); + } + + if (argc > 1) { + upsdebugx(1, "Args ignored: '%s' etc.", argv[0]); + } + + /* Create dummy config files */ + f = fopen(test_conf, "w"); + if (!f) { + perror("fopen test_nutauth.conf"); + return 1; + } + fprintf(f, "USER = globaluser\n"); + fprintf(f, "PASS = globalpass\n"); + fprintf(f, "CERTVERIFY = 1\n"); + fprintf(f, "INCLUDE %s\n", include_conf); + fprintf(f, "[@localhost:12345]\n"); + fprintf(f, " USER = hostuser\n"); + fprintf(f, " FORCESSL = 1\n"); + fprintf(f, "[admin@localhost:12345]\n"); + fprintf(f, " PASS = adminpass\n"); + fprintf(f, " FORCESSL = 1\n"); + fclose(f); + + f = fopen(include_conf, "w"); + if (!f) { + perror("fopen test_include.conf"); + return 1; + } + fprintf(f, "[@otherhost]\n"); + fprintf(f, " USER = otheruser\n"); + fprintf(f, " CERTHOST = \"Other Server\"\n"); + fclose(f); + + if ((s = getenv("NUT_AUTHCONF_FILE"))) { + printf("=== FYI: Trying NUT_AUTHCONF_FILE='%s' just for kicks\n", s); + if (upscli_read_authconf_file(NULL, 0) != 1) { + fprintf(stderr, "INFO: Default read_authconf failed (user-provided config parsing failed)\n"); + } else { + printf("=== Parsed user configuration (debug view):\n"); + /* With "for_debug", show all fields (highlight NULLs) */ + num_sections = upscli_dump_authconf_list(NULL, 1, 1); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + } + } + + /* 1. Expected file read */ + printf("=== Reading '%s' generated for this test\n", test_conf); + if (upscli_read_authconf_file(test_conf, 1) != 1) { + fprintf(stderr, "not ok %d - read_authconf failed\n", ++testnum); + return 1; + } + printf("ok %d - read_authconf did not fail\n", ++testnum); + + /* 2. Expected printout 1 */ + printf("=== Parsed configuration (production view):\n"); + /* Not "for_debug", but how would this info look in a config file */ + num_sections = upscli_dump_authconf_list(NULL, 0, 1); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); + + /* 3. Expected printout 2 */ + printf("=== Parsed configuration (debug view):\n"); + /* With "for_debug", show all fields (highlight NULLs) */ + num_sections = upscli_dump_authconf_list(NULL, 1, 1); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); + + /* Test matching */ + printf("=== Testing matches...\n"); + + /* 4. Global match (no specific section for this host) */ + printf("Checking global match for '@somehost:port', and adding it to the list...\n"); + ac = upscli_get_authconf_item(NULL, "somehost", "port", 1); + if (ac) { + printf("Global match got user=%s\n", ac->user ? ac->user : "NULL"); + if (ac->user && strcmp(ac->user, "globaluser") == 0) { + printf("ok %d - Global match OK\n", ++testnum); + } else { + printf("not ok %d - Global match FAILED (wrong user)\n", ++testnum); + return 1; + } + } else { + printf("not ok %d - Global match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 5. Host default match, not saved */ + printf("Checking host default match for '@localhost:12345', not saved into list\n"); + ac5 = upscli_get_authconf_item(NULL, "localhost", "12345", 0); + if (ac5 && strcmp(ac5->user, "hostuser") == 0 && ac5->forcessl == 1 && ac5->certverify == 1) { + printf("ok %d - Host default match OK\n", ++testnum); + } else { + printf("not ok %d - Host default match FAILED\n", ++testnum); + if (ac5) + upscli_free_authconf_item(ac5); + return 1; + } + + /* 6. Exact match */ + printf("Checking exact match for 'admin@localhost:12345'\n"); + ac = upscli_get_authconf_item("admin", "localhost", "12345", 1); + if (ac) { + printf("Exact match: got user=%s pass=%s forcessl=%d\n", + ac->user ? ac->user : "NULL", + ac->pass ? ac->pass : "NULL", + ac->forcessl); + + if (ac->user && strcmp(ac->user, "admin") == 0 + && ac->pass && strcmp(ac->pass, "adminpass") == 0 + && ac->forcessl == 1 + ) { + printf("ok %d - Exact match OK\n", ++testnum); + } else { + printf("not ok %d - Exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "admin", "adminpass"); + return 1; + } + } else { + printf("not ok %d - Exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 7. Non-exact match */ + printf("Checking non-exact match for 'somebody@localhost:12345'\n"); + ac7 = upscli_get_authconf_item("somebody", "localhost", "12345", 0); + if (ac7) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac7->user ? ac7->user : "NULL", + ac7->pass ? ac7->pass : "NULL", + ac7->forcessl); + + if (ac7->user && strcmp(ac7->user, "somebody") == 0 + && ac7->pass && strcmp(ac7->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac7->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 8. Host default match, saved to list */ + printf("Checking host default match for '@localhost:12345' and saving into list\n"); + ac8 = upscli_get_authconf_item(NULL, "localhost", "12345", 1); + if (ac8 && strcmp(ac8->user, "hostuser") == 0 && ac8->forcessl == 1 && ac8->certverify == 1) { + printf("ok %d - Host default match OK\n", ++testnum); + } else { + printf("not ok %d - Host default match FAILED\n", ++testnum); + return 1; + } + + /* 9. Non-exact match, take 2 */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, and adding it to the list\n"); + ac9 = upscli_get_authconf_item("somebody", "localhost", "12345", 1); + if (ac9) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac9->user ? ac9->user : "NULL", + ac9->pass ? ac9->pass : "NULL", + ac9->forcessl); + + if (ac9->user && strcmp(ac9->user, "somebody") == 0 + && ac9->pass && strcmp(ac9->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac9->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 10. Same non-exact match */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, should be same pointer\n"); + ac = upscli_get_authconf_item("somebody", "localhost", "12345", 1); + if (ac) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac->user ? ac->user : "NULL", + ac->pass ? ac->pass : "NULL", + ac->forcessl); + + if (ac->user && strcmp(ac->user, "somebody") == 0 + && ac->pass && strcmp(ac->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + /* 11. Same non-exact match - continued */ + if (ac == ac9) { + printf("ok %d - Non-exact match OK and returned same pointer to item in the list\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (did not return same pointer to item in the list)\n", ++testnum); + return 1; + } + + /* 12. Same non-exact match but not in the list */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, but not adding to list, should be a different pointer\n"); + ac12 = upscli_get_authconf_item("somebody", "localhost", "12345", 0); + if (ac12) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac12->user ? ac12->user : "NULL", + ac12->pass ? ac12->pass : "NULL", + ac12->forcessl); + + if (ac12->user && strcmp(ac12->user, "somebody") == 0 + && ac12->pass && strcmp(ac12->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac12->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac12)\n", ++testnum); + return 1; + } + /* 13. Same non-exact match - continued */ + if (ac12 != ac9) { + printf("ok %d - Non-exact match OK and did not return same pointer to item in the list\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (returned same pointer to item in the list but should have been a clone)\n", ++testnum); + return 1; + } + + /* 14. Include match */ + printf("Checking include match for '@otherhost'\n"); + ac = upscli_get_authconf_item(NULL, "otherhost", NULL, 1); + snprintf(buf, sizeof(buf), "@otherhost:%u", (unsigned int)NUT_PORT); + if (ac + && ac->section && strcmp(ac->section, buf) == 0 + && ac->user && strcmp(ac->user, "otheruser") == 0 + && ac->certhost && strcmp(ac->certhost, "Other Server") == 0 + ) { + printf("ok %d - Include match OK\n", ++testnum); + } else { + if (ac) { + printf("not ok %d - Include match FAILED: got section=%s user=%s\n", + ++testnum, + ac->section ? ac->section : "NULL", + ac->user ? ac->user : "NULL"); + } else { + printf("not ok %d - Include match FAILED: no ac\n", ++testnum); + } + return 1; + } + + /* 15. No bogus hits */ + printf("Checking NO match for '@otherhost:portnum' other than global section\n"); + ac = upscli_find_authconf_item(NULL, "otherhost", "portnum"); + if (ac) { + if (!(ac->section) || !*(ac->section)) { + printf("ok %d - No bogus match OK: got global section\n", ++testnum); + } else { + printf("not ok %d - No bogus match FAILED: had a hit\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("ok %d - No bogus match kind of OK: got no ac\n", ++testnum); + } + + /* 16. Expected printout 3 */ + printf("=== Parsed configuration (production view) after several 'get' operations with results caching:\n"); + /* Not "for_debug", but how would this info look in a config file */ + num_sections = upscli_dump_authconf_list(NULL, 0, 1); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + /* Added '@somehost:port' and 'somebody@...' */ + printf("%sok %d - parsed 6 sections\n", num_sections == 6 ? "" : "not ", ++testnum); + + upscli_free_authconf_item(ac5); + upscli_free_authconf_item(ac7); + upscli_free_authconf_item(ac12); + /* do not free ac8 and ac9 - they are added to list */ + + upscli_free_authconf_list(); + unlink(test_conf); + unlink(include_conf); + + printf("All tests passed!\n"); + return 0; +}