diff --git a/NEWS.adoc b/NEWS.adoc index dbc95c6dbb..6bb324c912 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -58,6 +58,11 @@ https://github.com/networkupstools/nut/milestone/13 attempts on timeout errors, simplifying error recovery. [PR #3414] * Increased default TCP response timeout to 2000 ms. [PR #3418] + - `dummy-ups` driver updates: + * Added `authconf` driver parameter for repeater mode to control + authentication configuration discovery. It accepts `default`, `none`, + or a specific authconf file path. [issue #3329] + - `nutdrv_qx` driver updates: * Only claim a USB device as "supported" during discovery out of the box if `subdriver_command` was assigned (`1A86:7523` is used by CH340/341 @@ -126,6 +131,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 @@ -150,6 +180,9 @@ https://github.com/networkupstools/nut/milestone/13 much later (tell the sysadmin to increase `ulimit` or set up a more conservative `MAXCONN`). If there is a separate soft and hard limit, and `MAXCONN` exceeds the soft limit, try to raise the bar. [issue #3365] + * If SSL configuration was provided, but the server failed to apply some + aspect of that, it should now abort with an explanation (and not proceed + with insecure start-up like it could do before). [issue #3331, PR #3435] - Recipes, CI and helper script updates not classified above: * Introduced `ci_build.sh` settings and respective CI workflow settings diff --git a/UPGRADING.adoc b/UPGRADING.adoc index e4beed637a..fe3624d3ff 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -46,6 +46,22 @@ 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] +- `upsd` data server updates: + * If SSL configuration was provided, but the server failed to apply some + aspect of that, it should now abort with an explanation (and not proceed + with insecure start-up like it could do before). [issue #3331, PR #3435] + +- 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 9376f92158..80fe5d144e 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..2c7b9a68b8 --- /dev/null +++ b/clients/authconf.c @@ -0,0 +1,1333 @@ +/* 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). + * This looks similar to upscli_splitname() but reserves the option to evolve + * the supported section name syntax differently for the different purpose. + * TOTHINK: Combine the `host:port` logic with upscli_splitaddr() to + * de-duplicate IPv6 nuance handling etc.? + */ + const char *at = NULL, *colon = NULL, *bracket_open = NULL, *bracket_close = 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__); + errno = EINVAL; + return -1; + } + + if (!(*sect_name)) { + /* TOTHINK: Should this mean `localhost@NUT_PORT`? Or global? Probably neither. */ + upsdebugx(1, "%s: sect_name is empty", __func__); + errno = EINVAL; + return -1; + } + + at = strchr(sect_name, '@'); + colon = strchr(sect_name, ':'); + bracket_open = strchr(sect_name, '['); + bracket_close = strchr(sect_name, ']'); + + /* Sanity checks */ + if (at && colon && colon < at) { + upsdebugx(1, "%s: Invalid section header: colon ':' before at '@': '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + + /* IPv6 numeric addresses are a series of colon-separated hex digits wrapped in square brackets */ + if (bracket_open && colon && colon < bracket_open) { + upsdebugx(1, "%s: Invalid section header: colon ':' before opening bracket '[': '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + + if ( (bracket_open && !bracket_close) + || (!bracket_open && bracket_close) + ) { + upsdebugx(1, "%s: Invalid section header: single bracket '[' or ']' in text: '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + + if (bracket_close && colon && colon > bracket_close) { + upsdebugx(1, "%s: Invalid section header: first colon ':' is after closing bracket ']': '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + + if (bracket_close && !colon) { + upsdebugx(1, "%s: Invalid section header: brackets '[...]' present but no colon ':' inside: '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + + /* For valid IPv6 spelling, consider there should be several colons inside brackets */ + if (bracket_open && bracket_close && colon && colon > bracket_open && colon < bracket_close) { + /* There should be at most one more colon inside brackets + * Technically: up to 8 hex sections split by 7 colons, + * but long stretches of zeroes may collapse into "::" ONCE. + */ + colon = strchr(colon + 1, ':'); + if (!colon || colon > bracket_close) { + upsdebugx(1, "%s: Invalid section header: withh numeric IPv6 there must be multiple colons ':' inside brackets: '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + } + + /* For valid IPv6 spelling, consider the colon after brackets as the port separator (if any, NULL otherwise) */ + if (bracket_close) { + /* There should be at most one colon after brackets (may also be none) */ + colon = strchr(bracket_close + 1, ':'); + } + + if (colon && strchr(colon + 1, ':')) { + upsdebugx(1, "%s: Invalid section header: multiple colons ':' in text: '%s'", __func__, sect_name); + return -1; + } + + if (bracket_open && strchr(bracket_open + 1, '[')) { + upsdebugx(1, "%s: Invalid section header: multiple opening brackets '[' in text: '%s'", __func__, sect_name); + errno = EINVAL; + return -1; + } + + if (bracket_close && strchr(bracket_close + 1, ']')) { + upsdebugx(1, "%s: Invalid section header: multiple closing brackets ']' in text: '%s'", __func__, sect_name); + errno = EINVAL; + 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; + + /* forget leading '[' */ + sect_name = xstrdup(&arg[0][1]); + /* we would remove the LAST seen bracket (there may be some in text, in case of IPv6 addresses) */ + end_bracket = strrchr(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; + } + + /* forget trailing ']' and any characters after it (comments etc.)... + * although those should be separate parseconf arguments */ + *(char *)(end_bracket) = '\0'; + + 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..ea577fd963 --- /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; /* Path to trusted CA certificates; in case of NSS, this is the path to location of the NSS DB files used for all purposes */ + char *certfile; /* (OpenSSL only) Client certificate file for authentication to the server */ + char *certident; /* Client certificate identity (nickname, alias) */ + char *certpasswd; /* Password for key/cert storage */ + char *ssl_backend; /* "openssl"/"nss" */ + char *certhost; /* Expected certificate subject (common name) of that server's certificate */ + 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..56938d5957 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -25,10 +25,19 @@ #include "config.h" #include "nutclient.h" +#include "parseconf.h" #include #include #include +#include + +#ifndef WIN32 +# include +#else +# include +# define R_OK 4 +#endif /* TODO: Make it a run-time option like upsdebugx(), * probably with a verbosity level variable in each @@ -237,7 +246,7 @@ typedef struct { int verbose_mode; int verify_depth; int always_continue; - + /* In this context, hostname is by default a copy of Socket->_host.c_str(), * which should be freed (we should set hostname_allocated!=0) */ const char *hostname; @@ -1992,6 +2001,433 @@ bool Client::hasFeature(const Feature& feature) } } +/* + * + * AuthConf implementation + * + */ + +std::vector AuthConf::authconf_list; +AuthConf* AuthConf::global_defaults = nullptr; + +AuthConf::AuthConf(const std::string& section_name) + : section(section_name), certverify(-1), forcessl(-1) +{ +} + +AuthConf::AuthConf(const AuthConf& source, const std::string& section_name) + : section(section_name), user(source.user), pass(source.pass), + certpath(source.certpath), certfile(source.certfile), + certident(source.certident), certpasswd(source.certpasswd), + ssl_backend(source.ssl_backend), certhost(source.certhost), + certverify(source.certverify), forcessl(source.forcessl) +{ +} + +AuthConf::~AuthConf() +{ +} + +/*static*/ std::vector AuthConf::getAuthConfList() +{ + return authconf_list; +} + +static void set_authconf_val(AuthConf& conf, const std::string& var, const std::string& val) +{ + if (var == "user") { conf.user = val; } + else if (var == "password") { conf.pass = val; } + else if (var == "certpath") { conf.certpath = val; } + else if (var == "certfile") { conf.certfile = val; } + else if (var == "certident") { conf.certident = val; } + else if (var == "certpasswd") { conf.certpasswd = val; } + else if (var == "ssl_backend") { conf.ssl_backend = val; } + else if (var == "certhost") { conf.certhost = val; } + else if (var == "certverify") { + if (val == "on" || val == "yes" || val == "1") conf.certverify = 1; + else if (val == "off" || val == "no" || val == "0") conf.certverify = 0; + } + else if (var == "forcessl") { + if (val == "on" || val == "yes" || val == "1") conf.forcessl = 1; + else if (val == "off" || val == "no" || val == "0") conf.forcessl = 0; + } +} + +static int parse_authconf_file(const std::string& filename, int fatal_errors, bool global_scope, std::vector& authconf_list, AuthConf*& global_defaults); + +static void handle_authconf_args(size_t numargs, char **arg, AuthConf*& current_section, bool global_scope, std::vector& authconf_list, AuthConf*& global_defaults) +{ + if (numargs < 1) return; + + /* Section header [section] */ + if (arg[0][0] == '[' && arg[0][strlen(arg[0])-1] == ']') { + std::string sectname = arg[0]; + sectname = sectname.substr(1, sectname.length() - 2); + + if (sectname == "_global_defaults" || sectname.empty()) { + if (!global_defaults) { + global_defaults = new AuthConf(""); + authconf_list.push_back(*global_defaults); + // Re-point global_defaults to the one in the list + global_defaults = &authconf_list.back(); + } + current_section = global_defaults; + } else { + /* Check if section already exists */ + current_section = nullptr; + for (auto& ac : authconf_list) { + if (ac.section == sectname) { + current_section = ∾ + break; + } + } + if (!current_section) { + authconf_list.push_back(AuthConf(sectname)); + current_section = &authconf_list.back(); + } + } + return; + } + + /* INCLUDE support */ + if (!strcasecmp(arg[0], "INCLUDE_REQUIRED")) { + if (numargs < 2) { + throw nut::IOException("INCLUDE_REQUIRED missing filename"); + } + parse_authconf_file(arg[1], 1, (current_section == nullptr), authconf_list, global_defaults); + return; + } + + if (!strcasecmp(arg[0], "INCLUDE")) { + if (numargs < 2) return; + parse_authconf_file(arg[1], 0, (current_section == nullptr), authconf_list, global_defaults); + return; + } + + if (numargs < 3 || strcmp(arg[1], "=") != 0) return; + + std::string var = arg[0]; + std::string val = arg[2]; + + if (!current_section) { + if (global_scope) { + if (!global_defaults) { + global_defaults = new AuthConf(""); + authconf_list.push_back(*global_defaults); + global_defaults = &authconf_list.back(); + } + current_section = global_defaults; + } else { + return; + } + } + + set_authconf_val(*current_section, var, val); +} + +static int parse_authconf_file(const std::string& filename, int fatal_errors, bool global_scope, std::vector& authconf_list, AuthConf*& global_defaults) +{ + PCONF_CTX_t ctx; + AuthConf* current_section = nullptr; + + if (pconf_init(&ctx, nullptr) != 1) { + if (fatal_errors) throw nut::IOException("Failed to initialize parser"); + return -1; + } + if (pconf_file_begin(&ctx, filename.c_str()) != 1) { + pconf_finish(&ctx); + if (fatal_errors) throw nut::IOException("Can't open authconf file: " + filename); + return -1; + } + + while (pconf_file_next(&ctx) == 1) { + if (ctx.numargs > 0) { + handle_authconf_args(ctx.numargs, ctx.arglist, current_section, global_scope, authconf_list, global_defaults); + } + } + + pconf_finish(&ctx); + return 1; +} + +/*static*/ int AuthConf::readAuthConfFile(const std::string& filename, int fatal_errors) +{ + std::string fn = filename; + freeAuthConfList(); + + if (fn.empty()) { + char path[NUT_PATH_MAX + 1]; + const char *s; + + if ((s = getenv("NUT_AUTHCONF_FILE")) && access(s, R_OK) == 0) { + fn = s; + } else if ((s = getenv("NUT_AUTHCONF_PATH")) && (snprintf(path, sizeof(path), "%s/nutauth.conf", s) > 0) && access(path, R_OK) == 0) { + fn = path; + } else if ((s = getenv("HOME"))) { + snprintf(path, sizeof(path), "%s/.config/nut/nutauth.conf", s); + if (access(path, R_OK) == 0) { + fn = path; + } else { + snprintf(path, sizeof(path), "%s/.nutauth.conf", s); + if (access(path, R_OK) == 0) fn = path; + } + } + + if (fn.empty()) { + snprintf(path, sizeof(path), "%s/nutauth.conf", confpath()); + if (access(path, R_OK) == 0) fn = path; + } + } + + if (fn.empty()) { + if (fatal_errors) { + throw nut::IOException("Can't open a user/site-provided default nutauth.conf file"); + } + return -1; + } + + return parse_authconf_file(fn, fatal_errors, true, authconf_list, global_defaults); +} + +static int splitaddr(const std::string& buf, std::string& hostname, uint16_t& port) +{ + if (buf.empty()) return -1; + + std::string tmp = buf; + size_t at = tmp.find('@'); + if (at != std::string::npos) { + /* matches upscli_splitaddr warning */ + fprintf(stderr, "splitaddr: wrong call? Got upsname@hostname[:port] string where only hostname[:port] was expected: %s\n", buf.c_str()); + tmp = tmp.substr(at + 1); + } + + size_t colon = std::string::npos; + if (tmp[0] == '[') { + size_t close_bracket = tmp.find(']'); + if (close_bracket == std::string::npos) { + fprintf(stderr, "splitaddr: missing closing bracket in [domain literal]: %s\n", buf.c_str()); + return -1; + } + hostname = tmp.substr(1, close_bracket - 1); + + size_t next_colon = tmp.find(':', close_bracket); + if (next_colon != std::string::npos && next_colon == close_bracket + 1) { + colon = next_colon; + } else { + port = NUT_PORT; + return 0; + } + } else { + colon = tmp.find(':'); + if (colon != std::string::npos) { + hostname = tmp.substr(0, colon); + } else { + hostname = tmp; + port = NUT_PORT; + return 0; + } + } + + if (colon != std::string::npos && colon + 1 < tmp.length()) { + std::string portstr = tmp.substr(colon + 1); + char* endptr = nullptr; + long l = strtol(portstr.c_str(), &endptr, 10); + if (endptr && *endptr == '\0' && l > 0 && l <= 65535) { + port = static_cast(l); + } else { + fprintf(stderr, "splitaddr: invalid port number specified after ':' separator: %s\n", buf.c_str()); + return -1; + } + } else { + fprintf(stderr, "splitaddr: no port number specified after ':' separator: %s\n", buf.c_str()); + return -1; + } + + return 0; +} + +static void normalize_parts(std::string& normalized_name, std::string& user, std::string& host, std::string& port) +{ + if (host.empty()) host = "localhost"; + if (port.empty()) { + char portbuf[16]; + snprintf(portbuf, sizeof(portbuf), "%u", static_cast(NUT_PORT)); + port = portbuf; + } + + normalized_name = ""; + if (!user.empty()) { + normalized_name += user + "@"; + } + + if ((host.find(':') != std::string::npos || host.find('[') != std::string::npos || host.find(']') != std::string::npos) + && host.front() != '[') { + normalized_name += "[" + host + "]"; + } else { + normalized_name += host; + } + + normalized_name += ":" + port; +} + +void AuthConf::merge(const AuthConf& source) +{ + if (section.empty() && !source.section.empty()) { + section = source.section; + } + + size_t at = section.find('@'); + if (at != std::string::npos && at > 0) { + /* Section title strictly defines a user name */ + user = section.substr(0, at); + } else if (at == 0) { + /* Section starts with @, so no user in title */ + } else { + /* No '@' in target section title */ + if (user.empty() && !source.user.empty()) { + user = source.user; + } + } + + if (pass.empty() && !source.pass.empty()) { + pass = source.pass; + } + if (certpath.empty() && !source.certpath.empty()) { + certpath = source.certpath; + } + if (certfile.empty() && !source.certfile.empty()) { + certfile = source.certfile; + } + if (certident.empty() && !source.certident.empty()) { + certident = source.certident; + } + if (certpasswd.empty() && !source.certpasswd.empty()) { + certpasswd = source.certpasswd; + } + if (ssl_backend.empty() && !source.ssl_backend.empty()) { + ssl_backend = source.ssl_backend; + } + if (certhost.empty() && !source.certhost.empty()) { + certhost = source.certhost; + } + if (certverify < 0 && source.certverify >= 0) { + certverify = source.certverify; + } + if (forcessl < 0 && source.forcessl >= 0) { + forcessl = source.forcessl; + } +} + +/*static*/ AuthConf AuthConf::findAuthConf(const std::string& user, const std::string& host, const std::string& port) +{ + if (authconf_list.empty()) return global_defaults ? *global_defaults : AuthConf(); + + if (user.empty() && host.empty() && port.empty()) { + if (global_defaults) return *global_defaults; + for (const auto& ac : authconf_list) { + if (ac.section.empty()) return ac; + } + return AuthConf(); + } + + std::string norm_user = user; + std::string norm_host = host; + std::string norm_port = port; + std::string normalized_name; + normalize_parts(normalized_name, norm_user, norm_host, norm_port); + + for (const auto& ac : authconf_list) { + if (ac.section == normalized_name) return ac; + } + + // Retry without user if we have one (host defaults) + size_t at = normalized_name.find('@'); + if (at != std::string::npos) { + std::string userless_name = normalized_name.substr(at); + if (userless_name.length() > 1) { // more than just '@' + for (const auto& ac : authconf_list) { + if (ac.section == userless_name) return ac; + } + } + } + + return global_defaults ? *global_defaults : AuthConf(); +} + +/*static*/ AuthConf AuthConf::getAuthConf(const std::string& user, const std::string& host, const std::string& port, bool add_to_list) +{ + std::string norm_user = user; + std::string norm_host = host; + std::string norm_port = port; + std::string normalized_name; + normalize_parts(normalized_name, norm_user, norm_host, norm_port); + + AuthConf* retval_user = nullptr; + AuthConf* retval_host = nullptr; + + for (auto& ac : authconf_list) { + if (ac.section == normalized_name) { + retval_user = ∾ + break; + } + } + + size_t at = normalized_name.find('@'); + if (at != std::string::npos) { + std::string userless_name = normalized_name.substr(at); + for (auto& ac : authconf_list) { + if (ac.section == userless_name) { + retval_host = ∾ + break; + } + } + } + + if (add_to_list) { + if (!retval_user) { + authconf_list.push_back(AuthConf(normalized_name)); + retval_user = &authconf_list.back(); + } + + if (retval_host) { + retval_user->merge(*retval_host); + } + if (global_defaults) { + retval_user->merge(*global_defaults); + } + if (!user.empty()) { + retval_user->user = user; + } + return *retval_user; + } + + AuthConf res(normalized_name); + if (retval_user) { + res.merge(*retval_user); + } + + if (retval_host) { + res.merge(*retval_host); + } + + if (global_defaults) { + res.merge(*global_defaults); + } + + // Ensure the user we requested is set if it was a fixed user + if (!user.empty()) { + res.user = user; + } + + return res; +} + +/*static*/ void AuthConf::freeAuthConfList() +{ + authconf_list.clear(); + global_defaults = nullptr; +} + /* * * TCP Client implementation @@ -2571,23 +3007,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 +3051,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 +3078,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 +3260,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 +3280,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; } } @@ -3229,6 +3678,27 @@ void TcpClient::connect(const std::string& host, uint16_t port, bool tryssl) connect(host, port); } +void TcpClient::connect(const AuthConf& ac) +{ + std::string hostname; + uint16_t portnum = NUT_PORT; + if (splitaddr(ac.section, hostname, portnum) == 0) { + _host = !hostname.empty() ? hostname : "localhost"; + _port = portnum; + } + + _tryssl = (ac.forcessl != 0); + + if (ac.ssl_backend == "nss") { + setSSLConfig_NSS(ac.forcessl, ac.certverify, ac.certpath, ac.certpasswd, "", ac.certident, "", ac.certhost); + } else { + /* Default to OpenSSL if not specified or "openssl" */ + setSSLConfig_OpenSSL(ac.forcessl, ac.certverify, ac.certpath, "", ac.certfile, "", "", ac.certident, "", ac.certhost); + } + + connect(); +} + void TcpClient::connect() { _socket->connect(_host, _port); @@ -3545,6 +4015,11 @@ void TcpClient::authenticate(const std::string& user, const std::string& passwd) detectError(sendQuery("PASSWORD " + passwd)); } +void TcpClient::authenticate(const AuthConf& ac) +{ + authenticate(ac.user, ac.pass); +} + void TcpClient::logout() { detectError(sendQuery("LOGOUT")); diff --git a/clients/nutclient.h b/clients/nutclient.h index e1c544aae9..3dd11b0182 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -87,6 +87,79 @@ class Device; class Variable; class Command; +/** + * Authentication configuration for a NUT client. + */ +class AuthConf +{ +public: + AuthConf(const std::string& section = ""); + AuthConf(const AuthConf& source, const std::string& section = ""); + ~AuthConf(); + + /** Get the one global list of all parsed authentication configurations */ + static std::vector getAuthConfList(); + + /** Read the authentication configuration file (usually nutauth.conf) */ + static int readAuthConfFile(const std::string& filename = "", int fatal_errors = 0); + + /** Find the best matching authconf for a given connection string */ + static AuthConf findAuthConf(const std::string& user, const std::string& host, const std::string& 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 empty, return the global section or empty if none such in the list. + */ + static AuthConf getAuthConf(const std::string& user, const std::string& host, const std::string& port, bool add_to_list = false); + + /** Merge contents of another configuration item into this one. + * Follows C upscli_merge_authconf_item() logic. + */ + void merge(const AuthConf& source); + + /** Clear the global list of authentication configurations */ + static void freeAuthConfList(); + + /** [@host:port] or [user@host:port], or empty for global defaults */ + std::string section; + + std::string user; + std::string pass; + + /** Path to trusted CA certificates; + * in case of NSS, this is the path to location + * of the NSS DB files used for all purposes */ + std::string certpath; + + /** (OpenSSL only) Client certificate file for authentication to the server */ + std::string certfile; + + /** Client certificate identity (nickname, alias) */ + std::string certident; + + /** Password for key/cert storage */ + std::string certpasswd; + + /** "openssl"/"nss" */ + std::string ssl_backend; + + /** 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) */ + std::string certhost; + int certverify; /* -1 = unset, 0 = off, 1 = on */ + int forcessl; /* -1 = unset, 0 = off, 1 = on */ + +private: + static std::vector authconf_list; + static AuthConf* global_defaults; +}; + /** * Base class for certificate store location information * (where to find the files and how to open them). @@ -402,17 +475,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 +500,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 +516,7 @@ class SSLConfig_CERTHOST std::string _cert_subj; int _forcessl; int _certverify; + uint16_t _port; }; /** @@ -496,9 +575,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). @@ -813,6 +893,12 @@ class Client */ virtual void authenticate(const std::string& user, const std::string& passwd) = 0; + /** + * Authenticate to a NUTD server using an AuthConf object. + * \param ac AuthConf object. + */ + virtual void authenticate(const AuthConf& ac) = 0; + /** * Disconnect from the NUTD server. * \todo Is his method is global to all connection protocol or is it specific to TCP ? @@ -1116,6 +1202,12 @@ class TcpClient : public Client */ void connect(const std::string& host, uint16_t port, bool tryssl); + /** + * Connect to the specified server using an AuthConf object. + * \param ac AuthConf object. + */ + void connect(const AuthConf& ac); + /** * Connect to the server. * Host name and ports must have already set (useful for reconnection). @@ -1162,6 +1254,7 @@ class TcpClient : public Client uint16_t getPort()const; virtual void authenticate(const std::string& user, const std::string& passwd) override; + virtual void authenticate(const AuthConf& ac) override; virtual void logout() override; virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) override; diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 747f52b2ca..8244924f5e 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -49,6 +49,9 @@ class MemClientStub : public Client NUT_UNUSED_VARIABLE(user); NUT_UNUSED_VARIABLE(passwd); } + virtual void authenticate(const AuthConf& ac) override { + NUT_UNUSED_VARIABLE(ac); + } virtual void logout() override {} virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) override; diff --git a/clients/upsc.c b/clients/upsc.c index c2c002a3b0..7fc0d61af6 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"); @@ -403,9 +408,12 @@ int main(int argc, char **argv) const struct timeval *upslog_start_tmp = upscli_upslog_start_sync(upslog_start_sync(NULL), nut_common_cookie()); int opt_ret = 0; uint16_t port; + upscli_authconf_t *ac_conn = NULL; 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 +463,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 +502,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,12 +551,22 @@ int main(int argc, char **argv) upsdebugx(1, "upsname='%s' hostname='%s' port='%" PRIu16 "'", NUT_STRARG(upsname), NUT_STRARG(hostname), port); + ac_conn = upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1); + if (ac_conn && upscli_init_authconf(ac_conn) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + } + 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)); } + /* Best-effort login (if present in the file) */ + if (ac_conn && ac_conn->user && ac_conn->pass) + upscli_authenticate_authconf(ups, ac_conn); + if (varlist) { upsdebugx(1, "Calling list_upses()"); list_upses(verbose); diff --git a/clients/upsclient.c b/clients/upsclient.c index ed7ba58515..587a14cd98 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -26,6 +26,8 @@ #include "nut_platform.h" #ifndef WIN32 +# include +# include # ifdef HAVE_PTHREAD /* this include is needed on AIX to have errno stored in thread local storage */ # include @@ -157,13 +159,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; @@ -172,6 +178,40 @@ static int upscli_initialized = 0; static struct timeval upscli_default_connect_timeout = {0, 0}; static int upscli_default_connect_timeout_initialized = 0; +#ifndef WITH_THREADING +# define WITH_THREADING 0 +#endif + +#if !WITH_THREADING +/* Not detected or actively disabled in configure script */ +# ifdef HAVE_PTHREAD +# undef HAVE_PTHREAD +# endif +# ifdef HAVE_SEMAPHORE_UNNAMED +# undef HAVE_SEMAPHORE_UNNAMED +# endif +# ifdef HAVE_SEMAPHORE_NAMED +# undef HAVE_SEMAPHORE_NAMED +# endif +#endif + +#ifdef HAVE_PTHREAD +# include + +# if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) +# include +# endif + +# ifdef HAVE_SEMAPHORE_NAMED +# ifdef HAVE_FCNTL_H +# include /* For O_* constants with sem_open() */ +# endif +# ifdef SYS_STAT_H +# include /* For mode constants */ +# endif +# endif +#endif + #ifdef WITH_OPENSSL static SSL_CTX *ssl_ctx; #endif /* WITH_OPENSSL */ @@ -181,6 +221,10 @@ static int verify_certificate = 1; #endif /* WITH_NSS */ #if defined(WITH_OPENSSL) || defined(WITH_NSS) +# ifdef HAVE_PTHREAD +static pthread_mutex_t mutex_host_cert; +# endif /* HAVE_PTHREAD */ + static HOST_CERT_t *first_host_cert = NULL; static char* sslcertname = NULL; static char* sslcertpasswd = NULL; @@ -310,16 +354,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 +448,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 +461,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 +860,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 +877,132 @@ 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 */ +int upscli_authconf_update_conn_flags(const upscli_authconf_t *ac, int *flags) +{ + if (!ac || !flags) + return 0; + + upsdebugx(4, "%s: finished with flags_ssl=0x%02X (try=%d req=%d ver=%d), " + "authconf: forcessl=%d certverify=%d", + __func__, (unsigned int)(*flags), + *flags & UPSCLI_CONN_TRYSSL ? 1 : 0, + *flags & UPSCLI_CONN_REQSSL ? 1 : 0, + *flags & UPSCLI_CONN_CERTVERIF ? 1 : 0, + ac->forcessl, ac->certverify); + + /* Initial default is usually "TRYSSL". + * Does the user force or avoid SSL mode? */ + switch (ac->forcessl) { + case 0: /* Do not try, rather require */ + *flags &= ~UPSCLI_CONN_TRYSSL; + *flags &= ~UPSCLI_CONN_REQSSL; + break; + case 1: /* Neither require nor even try, explicitly */ + *flags &= ~UPSCLI_CONN_TRYSSL; + *flags |= UPSCLI_CONN_REQSSL; + break; + case -1: /* Keep previous value, no override at this level */ + /* *flags |= UPSCLI_CONN_TRYSSL; */ + break; + default: break; + } + + switch (ac->certverify) { + case 0: *flags &= ~UPSCLI_CONN_CERTVERIF; break; + case 1: *flags |= UPSCLI_CONN_CERTVERIF; break; + case -1: break; + default: break; + } + + upsdebugx(4, "%s: finished with flags_ssl=0x%02X (try=%d req=%d ver=%d)", + __func__, (unsigned int)(*flags), + *flags & UPSCLI_CONN_TRYSSL ? 1 : 0, + *flags & UPSCLI_CONN_REQSSL ? 1 : 0, + *flags & UPSCLI_CONN_CERTVERIF ? 1 : 0); + + /* Operation did not fail */ + return 1; +} + +/** 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 +1050,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 +1320,134 @@ 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)); - cert->next = first_host_cert; + 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->host = xstrdup(hostname); + cert->port = port ? port : NUT_PORT; cert->certname = xstrdup(certname); cert->certverify = certverify; cert->forcessl = forcessl; + + /* Insert to head of list */ +# ifdef HAVE_PTHREAD + pthread_mutex_lock(&mutex_host_cert); +# endif + cert->next = first_host_cert; first_host_cert = cert; +# ifdef HAVE_PTHREAD + pthread_mutex_unlock(&mutex_host_cert); +# endif + #else NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certverify); NUT_UNUSED_VARIABLE(forcessl); @@ -1091,26 +1456,192 @@ 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) { +# ifdef HAVE_PTHREAD + pthread_mutex_lock(&mutex_host_cert); +# endif + 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); +# ifdef HAVE_PTHREAD + pthread_mutex_unlock(&mutex_host_cert); +# endif return cert; } cert = cert->next; } + +# ifdef HAVE_PTHREAD + pthread_mutex_unlock(&mutex_host_cert); +# endif } + 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 + +void upscli_free_host_cert(const char* hostname, const char* certname) +{ + 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; + } + } + } + + upscli_free_host_port_cert( + substr_port ? host : hostname, + port, certname); +} + +#if defined(WITH_OPENSSL) || defined(WITH_NSS) +static void upscli_free_host_port_cert_data(HOST_CERT_t* cert) +{ + if (!cert) + return; + + free((void*)cert->host); + free((void*)cert->certname); + + /* Don't let consumers with a copy get any funny ideas about memory we no longer own */ + cert->host = NULL; + cert->certname = NULL; + cert->next = NULL; +} +#endif /* SSL */ + +void upscli_free_host_port_cert(const char* hostname, uint16_t port, const char* certname) +{ +#if defined(WITH_OPENSSL) || defined(WITH_NSS) + HOST_CERT_t* cert = first_host_cert, *next = NULL, *prev = NULL; + + if (cert != NULL) { +# ifdef HAVE_PTHREAD + pthread_mutex_lock(&mutex_host_cert); +# endif + + while (cert != NULL) { + next = cert->next; + + if (cert->host != NULL + && strcmp(cert->host, hostname) == 0 + && cert->port == port + && (!certname || strcmp(cert->certname, certname) == 0) + ) { + if (prev) + prev->next = next; + + upscli_free_host_port_cert_data(cert); + free(cert); + + if (certname) { +# ifdef HAVE_PTHREAD + pthread_mutex_unlock(&mutex_host_cert); +# endif + return; + } + } + + prev = cert; + cert = next; + } + +# ifdef HAVE_PTHREAD + pthread_mutex_unlock(&mutex_host_cert); +# endif + } +#else /* ! SSL */ + NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); + NUT_UNUSED_VARIABLE(certname); +#endif /* ! SSL */ +} + +void upscli_free_host_cert_list(void) +{ +#if defined(WITH_OPENSSL) || defined(WITH_NSS) + HOST_CERT_t* cert = first_host_cert, *next = NULL; + + if (cert != NULL) { +# ifdef HAVE_PTHREAD + pthread_mutex_lock(&mutex_host_cert); +# endif + + while (cert != NULL) { + next = cert->next; + upscli_free_host_port_cert_data(cert); + free(cert); + cert = next; + } + +# ifdef HAVE_PTHREAD + pthread_mutex_unlock(&mutex_host_cert); +# endif + } +#endif /* ! SSL */ +} + int upscli_cleanup(void) { #ifdef WITH_OPENSSL @@ -1135,6 +1666,8 @@ int upscli_cleanup(void) PL_ArenaFinish(); #endif /* WITH_NSS */ + upscli_free_host_cert_list(); + upscli_free_authconf_list(); upscli_initialized = 0; return 1; } @@ -1569,10 +2102,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 +2129,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 +2206,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 +2385,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 +2421,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 +2680,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. */ @@ -2277,7 +2803,8 @@ static int upscli_errcheck(UPSCONN_t *ups, char *buf) /* look it up in the table */ for (i = 0; upsd_errlist[i].text != NULL; i++) { if (!strncmp(&buf[4], upsd_errlist[i].text, - strlen(upsd_errlist[i].text))) { + strlen(upsd_errlist[i].text)) + ) { ups->upserror = upsd_errlist[i].errnum; return -1; } @@ -2907,6 +3434,140 @@ int upscli_upserror(UPSCONN_t *ups) return ups->upserror; } +int upscli_authenticate(UPSCONN_t *ups, const char *username, const char *password, + int check_os_user, int ask_password) +{ + char buf[UPSCLI_NETBUF_LEN], user[UPSCLI_NETBUF_LEN - 12], pass[UPSCLI_NETBUF_LEN - 12]; + const char *user_ptr = username; + const char *pass_ptr = password; + size_t len; + + if (!ups) { + return -1; + } + + if (!user_ptr && check_os_user) { + struct passwd *pw = getpwuid(getuid()); + + if (pw) { + printf("Username (%s): ", pw->pw_name); + } else { + printf("Username: "); + } + + memset(user, '\0', sizeof(user)); + if (!fgets(user, sizeof(user), stdin)) { + upslog_with_errno(LOG_ERR, "Error reading from stdin!"); + ups->upserror = UPSCLI_ERR_INVUSERNAME; + return -1; + } + + /* deal with that pesky newline from fgets() */ + len = strlen(user); + while (len > 1) { + if (user[len - 1] == '\n' || user[len - 1] == '\r') { + user[len - 1] = '\0'; + len--; + continue; + } + break; + } + + if (!len) { + /* blank-line input */ + if (!pw) { + upslogx(LOG_ERR, "No username available - even tried getpwuid"); + ups->upserror = UPSCLI_ERR_USERREQUIRED; + return -1; + } + /* User accepted the proposed default from pw */ + snprintf(user, sizeof(user), "%s", pw->pw_name); + } + user_ptr = user; + } + + if (!pass_ptr && ask_password) { + /* NOTE: getpass leaks slightly - use -p when testing in valgrind. + * Generally using getpass() or getpass_r() might not be a + * good idea here (marked obsolete in POSIX) so we macro + * between the available options (also getpassphrase etc). + */ + char *pwtmp = GETPASS("Password: "); + if (!pwtmp) { + upslog_with_errno(LOG_ERR, "getpass failed"); + ups->upserror = UPSCLI_ERR_INVPASSWORD; + return -1; + } + snprintf(pass, sizeof(pass), "%s", pwtmp); + pass_ptr = pass; + } + + if (!user_ptr || !pass_ptr) { + upslogx(LOG_ERR, "Got this far without a username or password, this should not have happened"); + return -1; + } + + /* We have enough strings to try and log in */ + snprintf(buf, sizeof(buf), "USERNAME %s\n", user_ptr); + if (upscli_sendline(ups, buf, strlen(buf)) < 0) { + upslogx(LOG_ERR, "Can't set username: %s", upscli_strerror(ups)); + return -2; + } + + if (upscli_readline(ups, buf, sizeof(buf)) < 0 || upscli_errcheck(ups, buf) < 0) { + if (upscli_upserror(ups) != UPSCLI_ERR_UNKCOMMAND) { + upslogx(LOG_ERR, "Set username failed: %s", upscli_strerror(ups)); + } else { + upslogx(LOG_ERR, + "Set username failed due to an unknown command.\n" + "You probably need to upgrade upsd."); + } + return -2; + } + + /* catch insanity from the server - not ERR and not OK either */ + if (strncmp(buf, "OK", 2) != 0) { + upslogx(LOG_ERR, "Set username failed with unexpected protocol response: %s", buf); + ups->upserror = UPSCLI_ERR_PROTOCOL; + return -2; + } + + snprintf(buf, sizeof(buf), "PASSWORD %s\n", pass_ptr); + if (upscli_sendline(ups, buf, strlen(buf)) < 0) { + upslogx(LOG_ERR, "Can't set password: %s", upscli_strerror(ups)); + return -2; + } + + if (upscli_readline(ups, buf, sizeof(buf)) < 0 || upscli_errcheck(ups, buf) < 0) { + if (upscli_upserror(ups) != UPSCLI_ERR_UNKCOMMAND) { + upslogx(LOG_ERR, "Set password failed: %s", upscli_strerror(ups)); + } else { + upslogx(LOG_ERR, + "Set password failed due to an unknown command.\n" + "You probably need to upgrade upsd."); + } + return -2; + } + + /* catch insanity from the server - not ERR and not OK either */ + if (strncmp(buf, "OK", 2) != 0) { + upslogx(LOG_ERR, "Set password failed with unexpected protocol response: %s", buf); + ups->upserror = UPSCLI_ERR_PROTOCOL; + return -2; + } + + return 0; +} + +int upscli_authenticate_authconf(UPSCONN_t *ups, upscli_authconf_t *ac) +{ + if (!ac) { + ups->upserror = UPSCLI_ERR_INVALIDARG; + return -1; + } + return upscli_authenticate(ups, ac->user, ac->pass, 0, 0); +} + int upscli_ssl(UPSCONN_t *ups) { if (!ups) { diff --git a/clients/upsclient.h b/clients/upsclient.h index 7be5c8b537..e3d407caee 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,14 +153,22 @@ 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); +/* hostname may be a host:port; if certname is NULL, all list items are iterated */ +void upscli_free_host_cert(const char* hostname, const char* certname); +void upscli_free_host_port_cert(const char* hostname, uint16_t port, const char* certname); +void upscli_free_host_cert_list(void); + /* --- functions that only use the new names --- */ int upscli_get(UPSCONN_t *ups, size_t numq, const char **query, @@ -194,6 +203,40 @@ int upscli_upserror(UPSCONN_t *ups); * and check it against given expectations. */ int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re); +/** Common method to supply USERNAME and PASSWORD during the server dialog, + * whether pre-defined (CLI, authconf) or optionally queried interactively, + * for access to non-anonymous commands, variable settings, or data reads. + * Note that per NUT protocol, such authentication is only expected + * at most once per connection. + * + * Note this is separate from (but a prerequisite of) the LOGIN operation + * which allows a client like upsmon to gain a special role for a specific + * device, and perhaps further become a PRIMARY monitoring client for it. + * + * \param ups connection state + * \param username if NULL, we can optionally detect the username from the OS and query/confirm interactively + * \param password if NULL, we can optionally query for the password interactively + * \param check_os_user if 1, and username is NULL, try to get OS user name + * \param ask_password if 1, and password is NULL, try to ask for it on stdin + * + * \return 0 on success, -1 on argument error (failed to get fallback username + * and/or password), -2 on protocol error (failed when trying to use + * those values); check upscli_upserror() for details + */ +int upscli_authenticate(UPSCONN_t *ups, const char *username, const char *password, + int check_os_user, int ask_password); + +/** Equivalent (wrapper) for upscli_authenticate() with upscli_authconf_t + * which should convey definite "user" and "pass" field values + * (no interactive fallbacks here). + * + * \param ups connection state + * \param ac authentication configuration (user and pass fields are used) + * + * \return 0 on success, or -1 on error + */ +int upscli_authenticate_authconf(UPSCONN_t *ups, upscli_authconf_t *ac); + /* returns 1 if SSL mode is active for this connection */ int upscli_ssl(UPSCONN_t *ups); @@ -215,14 +258,16 @@ int upscli_ssl_caps(void); const char *upscli_ssl_caps_descr(void); void upscli_report_build_details(void); -/* Assign default upscli_connect() from string; return 0 if OK, or +/** Assign default upscli_connect() timeout from string (value + * in seconds, may be a fractional number); return 0 if OK, or * return -1 if parsing failed and current value was kept */ int upscli_set_default_connect_timeout(const char *secs); -/* If ptv!=NULL, populate it with a copy of last assigned internal timeout */ +/** If ptv!=NULL, populate it with a copy of last assigned internal timeout */ void upscli_get_default_connect_timeout(struct timeval *ptv); -/* Initialize default upscli_connect() timeout from a number of sources: +/** Initialize default upscli_connect() timeout from a number of sources: * built-in (0 = blocking), envvar NUT_DEFAULT_CONNECT_TIMEOUT, * or specified strings (may be NULL) most-preferred first. + * Non-NULL values are in seconds, may be fractional. * Returns 0 if any provided value was valid and applied, * or if none were provided so the built-in default was applied; * returns -1 if all provided values were not valid (so the built-in @@ -294,6 +339,9 @@ int upscli_init_default_connect_timeout(const char *cli_secs, const char *config #define UPSCLI_CONN_INET6 0x0008 /* IPv6 only */ #define UPSCLI_CONN_CERTVERIF 0x0010 /* Verify certificates for SSL */ +/** Update tryssl/reqssl/certverif bits according to authconf */ +int upscli_authconf_update_conn_flags(const upscli_authconf_t *ac, int *flags); + /****************************************************************************** * String methods for space-separated token lists, used originally in dstate * * These methods should ease third-party NUT clients' parsing of `ups.status` * diff --git a/clients/upscmd.c b/clients/upscmd.c index 5c14fb58e7..4f1259cf2a 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -24,7 +24,6 @@ #include "nut_platform.h" #ifndef WIN32 -#include #include #include #include @@ -54,7 +53,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 +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"); @@ -321,9 +323,10 @@ int main(int argc, char **argv) const struct timeval *upslog_start_tmp = upscli_upslog_start_sync(upslog_start_sync(NULL), nut_common_cookie()); int opt_ret = 0; uint16_t port; - ssize_t ret; + upscli_authconf_t *ac_conn = NULL; 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 +378,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 +424,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 +464,15 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ + ac_conn = upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1); + if (ac_conn && upscli_init_authconf(ac_conn) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + } + 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)); } @@ -460,73 +491,16 @@ int main(int argc, char **argv) fatalx(EXIT_FAILURE, "Error: old command names are not supported"); } - if (!have_un) { - struct passwd *pw; - - memset(username, '\0', sizeof(username)); - pw = getpwuid(getuid()); - - if (pw) { - printf("Username (%s): ", pw->pw_name); - } else { - printf("Username: "); + if (ac_conn && ac_conn->user && !have_un && ac_conn->pass && !have_pw) { + upsdebugx(1, "Using authentication from configuration file"); + if (upscli_authenticate_authconf(ups, ac_conn)) { + fatalx(EXIT_FAILURE, "Authentication failed: %s", upscli_strerror(ups)); } - - if (!fgets(username, sizeof(username), stdin)) { - fatalx(EXIT_FAILURE, "Error reading from stdin!"); - } - - /* deal with that pesky newline */ - if (strlen(username) > 1) { - username[strlen(username) - 1] = '\0'; - } else { - if (!pw) { - fatalx(EXIT_FAILURE, "No username available - even tried getpwuid"); - } - - snprintf(username, sizeof(username), "%s", pw->pw_name); - } - } - - /* getpass leaks slightly - use -p when testing in valgrind */ - if (!have_pw) { - /* using getpass or getpass_r might not be a - * good idea here (marked obsolete in POSIX) */ - char *pwtmp = GETPASS("Password: "); - - if (!pwtmp) { - fatalx(EXIT_FAILURE, "getpass failed: %s", strerror(errno)); - } - - snprintf(password, sizeof(password), "%s", pwtmp); - } - - snprintf(buf, sizeof(buf), "USERNAME %s\n", username); - - if (upscli_sendline(ups, buf, strlen(buf)) < 0) { - fatalx(EXIT_FAILURE, "Can't set username: %s", upscli_strerror(ups)); - } - - ret = upscli_readline(ups, buf, sizeof(buf)); - - if (ret < 0) { - if (upscli_upserror(ups) != UPSCLI_ERR_UNKCOMMAND) { - fatalx(EXIT_FAILURE, "Set username failed: %s", upscli_strerror(ups)); + } else { + upsdebugx(1, "Using authentication from CLI or interactive session"); + if (upscli_authenticate(ups, username, password, 1, 1) < 0) { + fatalx(EXIT_FAILURE, "Authentication failed: %s", upscli_strerror(ups)); } - - fatalx(EXIT_FAILURE, - "Set username failed due to an unknown command.\n" - "You probably need to upgrade upsd."); - } - - snprintf(buf, sizeof(buf), "PASSWORD %s\n", password); - - if (upscli_sendline(ups, buf, strlen(buf)) < 0) { - fatalx(EXIT_FAILURE, "Can't set password: %s", upscli_strerror(ups)); - } - - if (upscli_readline(ups, buf, sizeof(buf)) < 0) { - fatalx(EXIT_FAILURE, "Set password failed: %s", upscli_strerror(ups)); } /* enable status tracking ID */ diff --git a/clients/upsimage.c b/clients/upsimage.c index f9adbe8890..bf7a3dd5bd 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -626,8 +626,9 @@ 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; + upscli_authconf_t *ac_conn = NULL; double var = 0; #ifdef WIN32 @@ -680,9 +681,18 @@ 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); + ac_conn = upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1); + if (ac_conn && upscli_init_authconf(ac_conn) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + } + /* no 'host=' or 'display=' given */ if ((!monhost) || (!cmd)) noimage("No host or display"); @@ -699,7 +709,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 @@ -707,6 +717,14 @@ int main(int argc, char **argv) #endif } + /* TOTHINK #3411: Consider autologin via ac_conn->user/pass fields? + * Probably no, not for a web client anyone can interact with... + * This one is for a read-only listing, but could something be abused? + * If it comes to that, better fall back to requiring query/form args + * like in upsset.c + * //upscli_authenticate_authconf(&ups, ac_conn); + */ + for (i = 0; imgvar[i].name; i++) if (!strcmp(cmd, imgvar[i].name)) { diff --git a/clients/upslog.c b/clients/upslog.c index 80977a0179..a2a9e88fc8 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,21 @@ int main(int argc, char **argv) monhost_ups_current->port ); + /* FIXME [#3494]: 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) { + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + + // 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 +881,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 +1001,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 +1086,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/upsmon.c b/clients/upsmon.c index 962923b25c..46020fa04b 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -564,46 +564,12 @@ static int do_upsd_auth(utype_t *ups) { char buf[SMALLBUF]; - if (!ups->un) { - upslogx(LOG_ERR, "UPS [%s]: no username defined!", ups->sys); - return 0; - } - - snprintf(buf, sizeof(buf), "USERNAME %s\n", ups->un); - if (upscli_sendline(&ups->conn, buf, strlen(buf)) < 0) { - upslogx(LOG_ERR, "Can't set username on [%s]: %s", - ups->sys, upscli_strerror(&ups->conn)); - return 0; - } - - if (upscli_readline(&ups->conn, buf, sizeof(buf)) < 0) { - upslogx(LOG_ERR, "Set username on [%s] failed: %s", - ups->sys, upscli_strerror(&ups->conn)); - return 0; - } - - /* authenticate first */ - snprintf(buf, sizeof(buf), "PASSWORD %s\n", ups->pw); - - if (upscli_sendline(&ups->conn, buf, strlen(buf)) < 0) { - upslogx(LOG_ERR, "Can't set password on [%s]: %s", - ups->sys, upscli_strerror(&ups->conn)); - return 0; - } - - if (upscli_readline(&ups->conn, buf, sizeof(buf)) < 0) { - upslogx(LOG_ERR, "Set password on [%s] failed: %s", + if (upscli_authenticate(&ups->conn, ups->un, ups->pw, 0, 0) < 0) { + upslogx(LOG_ERR, "Authentication on [%s] failed: %s", ups->sys, upscli_strerror(&ups->conn)); return 0; } - /* catch insanity from the server - not ERR and not OK either */ - if (strncmp(buf, "OK", 2) != 0) { - upslogx(LOG_ERR, "Set password on [%s] failed - got [%s]", - ups->sys, buf); - return 0; - } - /* we require a upsname now */ if ((ups->upsname == NULL) || (strlen(ups->upsname) == 0)) { upslogx(LOG_ERR, "Login to UPS [%s] failed: empty upsname", diff --git a/clients/upsrw.c b/clients/upsrw.c index 636d868193..fb36eb21fb 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -24,7 +24,6 @@ #include "nut_platform.h" #ifndef WIN32 -#include #include #include #include @@ -54,7 +53,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 +77,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"); @@ -227,48 +229,9 @@ static void do_set(const char *varname, const char *newval) # pragma GCC diagnostic pop #endif -static void do_setvar(const char *varname, char *uin, const char *pass) +static void do_setvar(const char *varname) { - char newval[SMALLBUF], temp[SMALLBUF * 2], user[SMALLBUF], *ptr; - struct passwd *pw; - - if (uin) { - snprintf(user, sizeof(user), "%s", uin); - } else { - memset(user, '\0', sizeof(user)); - - pw = getpwuid(getuid()); - - if (pw) { - printf("Username (%s): ", pw->pw_name); - } else { - printf("Username: "); - } - - if (fgets(user, sizeof(user), stdin) == NULL) { - upsdebug_with_errno(LOG_INFO, "%s", __func__); - } - - /* deal with that pesky newline */ - if (strlen(user) > 1) { - user[strlen(user) - 1] = '\0'; - } else { - if (!pw) { - fatalx(EXIT_FAILURE, "No username available - even tried getpwuid"); - } - - snprintf(user, sizeof(user), "%s", pw->pw_name); - } - } - - /* leaks - use -p when running in valgrind */ - if (!pass) { - pass = GETPASS("Password: " ); - - if (!pass) { - fatal_with_errno(EXIT_FAILURE, "getpass failed"); - } - } + char newval[SMALLBUF], temp[SMALLBUF * 2], *ptr; /* Check if varname is in VAR=VALUE form */ if ((ptr = (char*)strchr(varname, '=')) != NULL) { @@ -283,31 +246,6 @@ static void do_setvar(const char *varname, char *uin, const char *pass) newval[strlen(newval) - 1] = '\0'; } - snprintf(temp, sizeof(temp), "USERNAME %s\n", user); - - if (upscli_sendline(ups, temp, strlen(temp)) < 0) { - fatalx(EXIT_FAILURE, "Can't set username: %s", upscli_strerror(ups)); - } - - if (upscli_readline(ups, temp, sizeof(temp)) < 0) { - - if (upscli_upserror(ups) == UPSCLI_ERR_UNKCOMMAND) { - fatalx(EXIT_FAILURE, "Set username failed due to an unknown command. You probably need to upgrade upsd."); - } - - fatalx(EXIT_FAILURE, "Set username failed: %s", upscli_strerror(ups)); - } - - snprintf(temp, sizeof(temp), "PASSWORD %s\n", pass); - - if (upscli_sendline(ups, temp, strlen(temp)) < 0) { - fatalx(EXIT_FAILURE, "Can't set password: %s", upscli_strerror(ups)); - } - - if (upscli_readline(ups, temp, sizeof(temp)) < 0) { - fatalx(EXIT_FAILURE, "Set password failed: %s", upscli_strerror(ups)); - } - /* no upsname means die */ if (!upsname) { fatalx(EXIT_FAILURE, "Error: a UPS name must be specified (upsname[@hostname[:port]])"); @@ -681,9 +619,11 @@ int main(int argc, char **argv) const struct timeval *upslog_start_tmp = upscli_upslog_start_sync(upslog_start_sync(NULL), nut_common_cookie()); int opt_ret = 0; uint16_t port; + upscli_authconf_t *ac_conn = NULL; 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 +673,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 +724,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,15 +764,34 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ + ac_conn = upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1); + if (ac_conn && upscli_init_authconf(ac_conn) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + } + 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)); } if (setvar) { /* setting a variable */ - do_setvar(setvar, username, password); + if (ac_conn && ac_conn->user && !username && ac_conn->pass && !password) { + upsdebugx(1, "Using authentication from configuration file"); + if (upscli_authenticate_authconf(ups, ac_conn)) { + fatalx(EXIT_FAILURE, "Authentication failed: %s", upscli_strerror(ups)); + } + } else { + upsdebugx(1, "Using authentication from CLI or interactive session"); + if (upscli_authenticate(ups, username, password, 1, 1) < 0) { + fatalx(EXIT_FAILURE, "Authentication failed: %s", upscli_strerror(ups)); + } + } + + /* VAR (interactive) or VAR=VAL */ + do_setvar(setvar); } else { /* if not, get the list of supported read/write variables */ print_rwlist(); diff --git a/clients/upsset.c b/clients/upsset.c index 20f70bf8b6..0e0ecfb55a 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,9 @@ static void clean_exit(void) int main(int argc, char **argv) { - char *s; - int i; + char *s, str_port[16]; + upscli_authconf_t *ac_conn = NULL; + int i; #ifdef WIN32 /* Required ritual before calling any socket functions */ @@ -1183,12 +1186,25 @@ 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(); - /* Nothing POSTed (or parsed correctly)? */ + ac_conn = upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1); + if (ac_conn && upscli_init_authconf(ac_conn) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + } + + /* Nothing POSTed (or parsed correctly)? + * TOTHINK: Consider autologin via ac_conn->user/pass fields? + * Probably no, not for a web client anyone can interact with... + * //upscli_authenticate_authconf(&ups, ac_conn); after a connect() + */ if ((!username) || (!password) || (!function)) loginscreen(); diff --git a/clients/upsstats.c b/clients/upsstats.c index 7d4450970a..778e726173 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,10 +540,38 @@ static void ups_connect(void) exit(EXIT_FAILURE); } - if (currups && upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) + /* 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) { + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + + // 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)); + } else { + /* TOTHINK #3411: Consider autologin via ac_conn->user/pass fields? + * Probably no, not for a web client anyone can interact with... + * This one is for a read-only listing, but could something be abused? + * If it comes to that, better fall back to requiring query/form args + * like in upsset.c + * //upscli_authenticate_authconf(&ups, ac_current); + */ + } lastups = currups; upsdebug_call_finished2(": pick first device on newly connected data server [%s]", @@ -1750,9 +1783,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 838ef58c89..85a6e2dc05 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 diff --git a/common/nutconf.cpp b/common/nutconf.cpp index 4345034f18..70804b7efb 100644 --- a/common/nutconf.cpp +++ b/common/nutconf.cpp @@ -2112,4 +2112,75 @@ bool UpsdUsersConfiguration::writeTo(NutStream & ostream) const return NutWriter::NUTW_OK == writer.writeConfig(*this); } + +NutAuthConfiguration::NutAuthConfiguration() + : GenericConfiguration() +{ +} + +bool NutAuthConfiguration::parseFrom(NutStream &istream) +{ + return GenericConfiguration::parseFrom(istream); +} + +bool NutAuthConfiguration::writeTo(NutStream &ostream) const +{ + return GenericConfiguration::writeTo(ostream); +} + + +NutAuthConfigParser::NutAuthConfigParser(const char *buffer) + : NutConfigParser(buffer), _config(nullptr), _currentSection("") +{ +} + +NutAuthConfigParser::NutAuthConfigParser(const std::string &buffer) + : NutConfigParser(buffer), _config(nullptr), _currentSection("") +{ +} + +void NutAuthConfigParser::parseNutAuthConfig(NutAuthConfiguration *config) +{ + _config = config; + parseConfig(config); +} + +void NutAuthConfigParser::onParseBegin() +{ + _currentSection = ""; +} + +void NutAuthConfigParser::onParseComment(const std::string &) +{ +} + +void NutAuthConfigParser::onParseSectionName(const std::string §ionName, const std::string &) +{ + _currentSection = sectionName; +} + +void NutAuthConfigParser::onParseDirective(const std::string &directiveName, char, const ConfigParamList &values, const std::string &) +{ + if (!_config) { + return; + } + + if (values.empty()) { + return; + } + + std::string section = _currentSection; + if (section.empty()) { + section = "*"; + } + + _config->set(section, directiveName, values); +} + +void NutAuthConfigParser::onParseEnd() +{ + _config = nullptr; +} + + } /* namespace nut */ 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 7de682323c..3d41ca5f34 100644 --- a/configure.ac +++ b/configure.ac @@ -7353,6 +7353,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 @@ -7397,6 +7398,7 @@ AC_CONFIG_FILES([ scripts/installer/Makefile scripts/misc/nut.bash_completion scripts/obs/Makefile + scripts/perl/UPS/Nut.pm scripts/python/Makefile scripts/python/module/Makefile scripts/python/module/PyNUT.py diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index abfc622420..75e4449ce9 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 \ @@ -532,6 +535,7 @@ HTML_CGI_MANS = $(INST_HTML_CGI_MANS) SRC_DEV_PAGES = \ upsclient.txt \ upscli_add_host_cert.txt \ + upscli_authenticate.txt \ upscli_cleanup.txt \ upscli_connect.txt \ upscli_disconnect.txt \ @@ -553,6 +557,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 +731,9 @@ $(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_authenticate.$(MAN_SECTION_API) \ + $(UPSCLI_AUTHENTICATE_DEPS) \ upscli_cleanup.$(MAN_SECTION_API) \ upscli_connect.$(MAN_SECTION_API) \ upscli_tryconnect.$(MAN_SECTION_API) \ @@ -749,6 +762,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 +819,18 @@ 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_AUTHENTICATE_DEPS = upscli_authenticate_authconf.$(MAN_SECTION_API) +$(UPSCLI_AUTHENTICATE_DEPS): upscli_authenticate.$(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 +846,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 $@ @@ -869,6 +920,7 @@ endif WITH_DEV INST_HTML_DEV_MANS = \ upsclient.html \ upscli_add_host_cert.html \ + upscli_authenticate.html \ upscli_cleanup.html \ upscli_connect.html \ upscli_disconnect.html \ @@ -890,6 +942,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 \ @@ -948,6 +1006,9 @@ HTML_DEV_MANS_FICTION = \ nutscan_scan_ip_range_ipmi.html \ nutscan_add_commented_option_to_device.html +upscli_authenticate_authconf.html: upscli_authenticate.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + upscli_readline_timeout.html upscli_readline_timeout_may_disconnect.html: upscli_readline.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ @@ -957,6 +1018,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/dummy-ups.txt b/docs/man/dummy-ups.txt index 6d86195657..e7dedff4a1 100644 --- a/docs/man/dummy-ups.txt +++ b/docs/man/dummy-ups.txt @@ -228,11 +228,26 @@ For instance: driver = dummy-ups port = ups1@remotehost desc = "dummy-ups in repeater mode" + authconf = default Unlike UPS specifications in the rest of NUT, the `@hostname` portion is not optional -- it is the `@` character which enables Repeater Mode. To refer to an UPS on the same host as *dummy-ups*, use `port = upsname@localhost`. +Since NUT v2.8.6 release, a new `authconf` driver parameter lets you +choose how the driver discovers authentication and trust parameters +for the repeated UPS connection: + +* `authconf = default` to require a user (see `RUN_AS_USER`) or system + supplied NUT authconf file. See linkman:nutauth.conf[5] man page + about default locations for the file. +* `authconf = none` to disable authconf parsing entirely. +* `authconf = ` to require a specific authconf file. + +If no value is specified, the driver will try to find and use +the file in default locations, but should not fail if that is +not possible. + Note that to avoid CPU overload with an infinite loop, the driver "sleeps" a bit between data-requesting cycles (currently this delay is hardcoded to one second), so propagation of data updates available to a remote `upsd` may lag 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/nutscan_scan_nut.txt b/docs/man/nutscan_scan_nut.txt index 88e11735ca..1797409fb7 100644 --- a/docs/man/nutscan_scan_nut.txt +++ b/docs/man/nutscan_scan_nut.txt @@ -23,6 +23,27 @@ SYNOPSIS nutscan_ip_range_list_t * irl, const char * port, useconds_t usec_timeout); + + /* Newer API */ + typedef struct nutscan_nut_authconf { + const char * authconf_file; /* where to load authconf data from; "none" means to ignore auth config even if it exists */ + useconds_t usec_timeout; /* Wait this long for a response */ + const char * port_string; /* We can pass a port name like "nut" and resolve it inside */ + + /* Added for consistency with other structs; not used at the moment; + * practically see also `struct nut_scan_arg` in `scan_nut.c`: */ + const char * peername; + uint16_t port_number; + } nutscan_nut_authconf_t; + + nutscan_device_t * nutscan_scan_nut_authconf( + const char * startIP, + const char * stopIP, + nutscan_nut_authconf_t *sec); + + nutscan_device_t * nutscan_scan_ip_range_nut_authconf( + nutscan_ip_range_list_t * irl, + nutscan_nut_authconf_t *sec); ------ DESCRIPTION diff --git a/docs/man/upsc.txt b/docs/man/upsc.txt index 215ffc25c5..6ebc5b8748 100644 --- a/docs/man/upsc.txt +++ b/docs/man/upsc.txt @@ -9,11 +9,11 @@ upsc - Lightweight read-only NUT client SYNOPSIS -------- -*upsc* [-j] -l | -L ['host'] +*upsc* [-j] [-A 'authconf'] -l | -L ['host'] -*upsc* [-j] 'ups' ['variable'] +*upsc* [-j] [-A 'authconf'] 'ups' ['variable'] -*upsc* [-j] -c 'ups' +*upsc* [-j] [-A 'authconf'] -c 'ups' DESCRIPTION ----------- @@ -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,30 @@ 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) to specify connection + security settings and/or credentials for this run. ++ +NOTE: Credentials are not currently required for read-only access to NUT. ++ +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 +186,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_authenticate.txt b/docs/man/upscli_authenticate.txt new file mode 100644 index 0000000000..3bbe1a67ed --- /dev/null +++ b/docs/man/upscli_authenticate.txt @@ -0,0 +1,57 @@ +UPSCLI_AUTHENTICATE(3) +====================== + +NAME +---- + +upscli_authenticate, upscli_authenticate_authconf - Common methods to supply +USERNAME and PASSWORD during the server dialog. + +SYNOPSIS +-------- + +---- + #include + + int upscli_authenticate(UPSCONN_t *ups, + const char *username, const char *password, + int check_os_user, int ask_password); + + int upscli_authenticate_authconf(UPSCONN_t *ups, upscli_authconf_t *ac); +---- + +DESCRIPTION +----------- + +The *upscli_authenticate()* function takes the pointer 'ups' to a *UPSCONN_t* +state, and attempts to submit credentials to the server. + +If 'username' is `NULL` and 'check_os_user' is `1`, it tries to detect the +username from the OS and queries the user to confirm this finding or enter +a different name on standard input. + +If 'password' is `NULL` and 'ask_password' is `1`, it queries for the password +interactively. + +The *upscli_authenticate_authconf()* function is an equivalent wrapper for +*upscli_authenticate()* that uses the 'user' and 'pass' fields from the +*upscli_authconf_t* structure 'ac', without the fallback toggle arguments. + +RETURN VALUE +------------ + +The *upscli_authenticate()* and *upscli_authenticate_authconf()* functions +return 0 on success, -1 if an error occurs during fallback input of username +or password, or -2 if an error occurs during dialog with the data server. + +In case of errors (negative returns), use linkman:upscli_upserror[3] and +linkman:upscli_strerror[3] to determine the problem code or description. +Note that the *upscli_authenticate()* function itself reports any details +it knows about the situation (typically to `stderr` and/or `syslog`) with +`upslogx(LOG_ERR, ...)` calls. + +SEE ALSO +-------- +linkman:upscli_connect[3], +linkman:upscli_strerror[3], +linkman:upscli_upserror[3] 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..a43057d6fa 100644 --- a/docs/man/upscmd.txt +++ b/docs/man/upscmd.txt @@ -13,7 +13,7 @@ SYNOPSIS *upscmd* -l 'ups' -*upscmd* [-u 'username'] [-p 'password'] [-w] [-t ] 'ups' 'command' +*upscmd* [-A 'authconf'] [-u 'username'] [-p 'password'] [-w] [-t ] 'ups' 'command' DESCRIPTION ----------- @@ -39,11 +39,18 @@ may not support any of them. *-u* 'username':: Set the username for the connection to the server. This is optional, and -you will be prompted for this when invoking a command if -u is not used. +a value from 'authconf' file would be used (if it resolves both a username +and a password for the host and port involved), otherwise you will be +prompted for this when invoking a command, if '-u' is not used. ++ +NUT usernames are defined in linkman:upsd.users[5] on the server side, +and are not linked to system usernames (although if a system user name can +be resolved, it would be the default proposed value for interactive mode). *-p* 'password':: Set the password to authenticate to the server. This is also optional -like -u, and you will be prompted for it if necessary. +like '-u', and will be used from 'authconf' or you will be prompted for +it if necessary. *-w*:: Wait for the completion of command execution by the driver and return its @@ -80,6 +87,28 @@ 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) to specify connection + security settings and/or credentials for this run. ++ +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 +161,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/upsd.conf.txt b/docs/man/upsd.conf.txt index 17f4a18e6f..360ac19578 100644 --- a/docs/man/upsd.conf.txt +++ b/docs/man/upsd.conf.txt @@ -256,6 +256,15 @@ or CLI options, regardless of older logging level being higher or lower than the newly found number; a missing (or commented away) value however does not change the previously active logging verbosity. +HARDENING +--------- + +Starting with NUT v2.8.6 release, if SSL configuration options are specified +(`CERTFILE` or `CERTPATH`, possibly `CERTIDENT` as applicable for the SSL +backend the program was built against), but the configuration was incomplete +or could not be applied (e.g. missing certificates), the server would refuse +to start. Earlier releases logged the error and proceeded in insecure fashion. + SEE ALSO -------- 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..97f9223339 100644 --- a/docs/man/upslog.txt +++ b/docs/man/upslog.txt @@ -155,6 +155,30 @@ 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) to specify connection + security settings and/or credentials for this run. ++ +NOTE: Credentials are not currently required for read-only access to NUT. ++ +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 +228,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 +245,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..71041ae998 100644 --- a/docs/man/upsrw.txt +++ b/docs/man/upsrw.txt @@ -13,7 +13,7 @@ SYNOPSIS *upsrw* -h -*upsrw* -s 'variable' [-u 'username'] [-p 'password'] [-w] [-t ] 'ups' +*upsrw* -s 'variable' [-A 'authconf'] [-u 'username'] [-p 'password'] [-w] [-t ] 'ups' DESCRIPTION ----------- @@ -35,8 +35,8 @@ OPTIONS Specify the variable to be changed inside the UPS. For unattended mode such as in shell scripts, use the format VAR=VALUE to specify both the variable and the value, for example: - - -s input.transfer.high=129 ++ + -s input.transfer.high=129 + Without this argument, upsrw will just display the list of the variables and their possible values. @@ -54,13 +54,19 @@ with other tools. *-u* 'username':: Set the NUT username for the connection to the server. This is optional, -and you will be prompted for this when using the -s option if you don't -specify -u on the command line. NUT usernames are defined in -linkman:upsd.users[5], and are not linked to system usernames. +a value from 'authconf' file would be used (if it resolves both a username +and a password for the host and port involved), otherwise you will be +prompted for this when using the -s option if you don't specify '-u' on +the command line. ++ +NUT usernames are defined in linkman:upsd.users[5] on the server side, +and are not linked to system usernames (although if a system user name can +be resolved, it would be the default proposed value for interactive mode). *-p* 'password':: Set the password to authenticate to the server. This is also optional -like -u, and you will be prompted for it if necessary. +like '-u', and will be used from 'authconf' or you will be prompted for +it if necessary. *-w*:: Wait for the completion of setting execution by the driver and return its @@ -97,6 +103,28 @@ 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) to specify connection + security settings and/or credentials for this run. ++ +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 +172,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 e9ecc757d3..359239eb50 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3780 utf-8 +personal_ws-1.1 en 3785 utf-8 AAC AAS ABI @@ -1246,6 +1246,7 @@ SPS SRC SRVS SSD +SSLBACKEND SSLContext SSSS STARTTLS @@ -1699,6 +1700,7 @@ authPassword authPriv authProtocol authType +authconf autoboot autoconf autodetect @@ -2208,6 +2210,7 @@ formatconfig formatstring fosshost fp +fprintf freebsd freedesktop freeipmi @@ -2856,6 +2859,7 @@ numa numbatteries numlogins numq +nutauth nutclient nutclientmem nutconf @@ -3310,6 +3314,7 @@ spectype spellcheck spellchecked splitaddr +splitauth splitname sprintf squasher diff --git a/drivers/dummy-ups.c b/drivers/dummy-ups.c index e618f89cfd..86c4ff97e7 100644 --- a/drivers/dummy-ups.c +++ b/drivers/dummy-ups.c @@ -48,7 +48,7 @@ #include "dummy-ups.h" #define DRIVER_NAME "Device simulation and repeater driver" -#define DRIVER_VERSION "0.25" +#define DRIVER_VERSION "0.26" /* driver description structure */ upsdrv_info_t upsdrv_info = @@ -159,23 +159,40 @@ void upsdrv_initinfo(void) } /* Connect to the target */ ups = (UPSCONN_t *)xmalloc(sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) - { - if(repeater_disable_strict_start == 1) + { /* scoping */ + upscli_authconf_t *ac_conn = NULL; + int flags_ssl = UPSCLI_CONN_TRYSSL; + char str_port[16]; + + ac_conn = upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1); + if (ac_conn && upscli_init_authconf(ac_conn) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + } + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { - upslogx(LOG_WARNING, "Warning: %s", upscli_strerror(ups)); + if (repeater_disable_strict_start == 1) + { + upslogx(LOG_WARNING, "Warning: %s", upscli_strerror(ups)); + } + else + { + fatalx(EXIT_FAILURE, "Error: %s. " + "Any errors encountered starting the repeater mode result in driver termination, " + "perhaps you want to set the 'repeater_disable_strict_start' option?", + upscli_strerror(ups)); + } } else { - fatalx(EXIT_FAILURE, "Error: %s. " - "Any errors encountered starting the repeater mode result in driver termination, " - "perhaps you want to set the 'repeater_disable_strict_start' option?" - , upscli_strerror(ups)); + upsdebugx(1, "Connected to %s@%s", client_upsname, hostname); + } + if (ac_conn && ac_conn->user && ac_conn->pass) { + upsdebugx(1, "%s: Using authentication from configuration file", __func__); + if (upscli_authenticate_authconf(ups, ac_conn) < 0) { + fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); + } } - } - else - { - upsdebugx(1, "Connected to %s@%s", client_upsname, hostname); } if (upsclient_update_vars() < 0) { @@ -192,9 +209,9 @@ void upsdrv_initinfo(void) else { fatalx(EXIT_FAILURE, "Error: %s. " - "Any errors encountered starting the repeater mode result in driver termination, " - "perhaps you want to set the 'repeater_disable_strict_start' option?" - , upscli_strerror(ups)); + "Any errors encountered starting the repeater mode result in driver termination, " + "perhaps you want to set the 'repeater_disable_strict_start' option?", + upscli_strerror(ups)); } } /* FIXME: commands and settable variable! */ @@ -455,6 +472,7 @@ void upsdrv_tweak_prognames(void) void upsdrv_makevartable(void) { addvar(VAR_VALUE, "mode", "Specify mode instead of guessing it from port value (dummy = dummy-loop, dummy-once, repeater)"); /* meta */ + addvar(VAR_VALUE, "authconf", "Select authentication config for repeater mode (default, none, or path to authconf file)"); addvar(VAR_FLAG, "repeater_disable_strict_start", "Do not terminate the driver encountering errors when starting the repeater mode"); } @@ -481,9 +499,30 @@ void upsdrv_initups(void) || (val && !strcmp(val, "repeater")) /*|| (val && !strcmp(val, "meta")) */ ) { + const char *authconf = dstate_getinfo("driver.parameter.authconf"); upsdebugx(1, "Repeater mode"); mode = MODE_REPEATER; dstate_setinfo("driver.parameter.mode", "repeater"); + + /* Handle authconf option in repeater mode */ + if (authconf) { + if (!strcmp(authconf, "none")) { + upsdebugx(1, "%s: Using authconf='%s': skipping auth config", __func__, authconf); + } else { + if (!strcmp(authconf, "default")) { + upsdebugx(1, "%s: Using authconf='%s': require a user or system provided file", __func__, authconf); + if (upscli_read_authconf_file(NULL, 1) < 0) + fatalx(EXIT_FAILURE, "Failed to parse auth configuration file"); + } else { + upsdebugx(1, "%s: Using authconf='%s': require this file", __func__, authconf); + if (upscli_read_authconf_file(authconf, 1) < 0) + fatalx(EXIT_FAILURE, "Failed to parse auth configuration file"); + } + } + } else { + upsdebugx(1, "%s: Using best-effort auth config detection", __func__); + upscli_read_authconf_file(NULL, 0); + } /* FIXME: if there is at least one more => MODE_META... */ } else 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 b50c14a0a3..6119daf865 100644 --- a/include/common.h +++ b/include/common.h @@ -424,6 +424,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/nutconf.hpp b/include/nutconf.hpp index 710bd56d02..67dc2c3bc8 100644 --- a/include/nutconf.hpp +++ b/include/nutconf.hpp @@ -2391,6 +2391,69 @@ class UpsdUsersConfiguration : public GenericConfiguration }; // end of class UpsdUsersConfiguration + +/** nutauth.conf configuration */ +class NutAuthConfiguration : public GenericConfiguration +{ +public: + NutAuthConfiguration(); + + // Required to allow NutAuthConfigParser::onParseDirective() access to set() + friend class NutAuthConfigParser; + + /** Section-specific configuration attributes getters and setters \{ */ + + inline std::string getUser(const std::string & section) const { return getStr(section, "user"); } + inline std::string getPassword(const std::string & section) const { return getStr(section, "password"); } + inline std::string getCertPath(const std::string & section) const { return getStr(section, "certpath"); } + inline std::string getCertFile(const std::string & section) const { return getStr(section, "certfile"); } + inline std::string getCertIdent(const std::string & section) const { return getStr(section, "certident"); } + inline std::string getCertPasswd(const std::string & section) const { return getStr(section, "certpasswd"); } + inline std::string getSslBackend(const std::string & section) const { return getStr(section, "ssl_backend"); } + inline std::string getCertHost(const std::string & section) const { return getStr(section, "certhost"); } + inline BoolInt getCertVerify(const std::string & section) const { return getBoolInt(section, "certverify", BoolInt(-1)); } + inline BoolInt getForceSsl(const std::string & section) const { return getBoolInt(section, "forcessl", BoolInt(-1)); } + + inline void setUser(const std::string & section, const std::string & val) { setStr(section, "user", val); } + inline void setPassword(const std::string & section, const std::string & val) { setStr(section, "password", val); } + inline void setCertPath(const std::string & section, const std::string & val) { setStr(section, "certpath", val); } + inline void setCertFile(const std::string & section, const std::string & val) { setStr(section, "certfile", val); } + inline void setCertIdent(const std::string & section, const std::string & val) { setStr(section, "certident", val); } + inline void setCertPasswd(const std::string & section, const std::string & val) { setStr(section, "certpasswd", val); } + inline void setSslBackend(const std::string & section, const std::string & val) { setStr(section, "ssl_backend", val); } + inline void setCertHost(const std::string & section, const std::string & val) { setStr(section, "certhost", val); } + inline void setCertVerify(const std::string & section, BoolInt val) { setBoolInt(section, "certverify", val); } + inline void setForceSsl(const std::string & section, BoolInt val) { setBoolInt(section, "forcessl", val); } + + /** \} */ + + /** Serialisable interface implementation overload \{ */ + bool parseFrom(NutStream & istream) override; + bool writeTo(NutStream & ostream) const override; + /** \} */ + +}; // end of class NutAuthConfiguration + + +class NutAuthConfigParser : public NutConfigParser +{ +public: + NutAuthConfigParser(const char* buffer = nullptr); + NutAuthConfigParser(const std::string& buffer); + + void parseNutAuthConfig(NutAuthConfiguration* config); +protected: + virtual void onParseBegin() override; + virtual void onParseComment(const std::string& comment) override; + virtual void onParseSectionName(const std::string& sectionName, const std::string& comment = "") override; + virtual void onParseDirective(const std::string& directiveName, char sep = 0, const ConfigParamList& values = ConfigParamList(), const std::string& comment = "") override; + virtual void onParseEnd() override; + + NutAuthConfiguration* _config; + std::string _currentSection; +}; + + } /* namespace nut */ #endif /* __cplusplus */ #endif /* NUTCONF_H_SEEN */ 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/scripts/Makefile.am b/scripts/Makefile.am index 09565e864e..1c341a6747 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -12,7 +12,7 @@ EXTRA_DIST = \ logrotate/nutlogd \ misc/nut.bash_completion.in \ misc/osd-notify \ - perl/UPS/Nut.pm \ + perl/UPS/Nut.pm.in \ perl/test_nutclient.pl \ RedHat/halt.patch \ RedHat/README.adoc \ 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/debian.rules b/scripts/obs/debian.rules index 361819aec8..67c6074524 100755 --- a/scripts/obs/debian.rules +++ b/scripts/obs/debian.rules @@ -64,7 +64,8 @@ NUTPKG_WITH_DMF := $(shell test -d scripts/DMF && echo 1 || echo 0) runbasedir := $(shell test -d /run && echo /run || echo /var/run) # FIXME: Find a smarter way to set those from main codebase recipes... -# Something like `git grep 'version-info' '*.am'` (note: here we use "$MAJOR - $AGE")? +# Something like `git grep 'version-info' '*.am'` +# WARNING: note here we use "$MAJOR - $AGE" (so for a `6.0.2` we put `4` here)! SO_MAJOR_LIBUPSCLIENT=7 SO_MAJOR_LIBNUTCLIENT=4 SO_MAJOR_LIBNUTCLIENTSTUB=1 diff --git a/scripts/obs/nut.spec b/scripts/obs/nut.spec index 158aca99c7..c669af0e0a 100644 --- a/scripts/obs/nut.spec +++ b/scripts/obs/nut.spec @@ -106,7 +106,8 @@ %define NUTPKG_WITH_DMF %( test -d scripts/DMF && echo 1 || echo 0 ) # FIXME: Find a smarter way to set those from main codebase recipes... -# Something like `git grep 'version-info' '*.am'` (note: here we use "$MAJOR - $AGE")? +# Something like `git grep 'version-info' '*.am'` +# WARNING: note here we use "$MAJOR - $AGE" (so for a `6.0.2` we put `4` here)! %define SO_MAJOR_LIBUPSCLIENT 7 %define SO_MAJOR_LIBNUTCLIENT 4 %define SO_MAJOR_LIBNUTCLIENTSTUB 1 @@ -573,8 +574,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 +692,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/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm.in similarity index 80% rename from scripts/perl/UPS/Nut.pm rename to scripts/perl/UPS/Nut.pm.in index 36ccc26d6d..5fa3026475 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm.in @@ -41,7 +41,13 @@ sub new { # Author: Kit Peters my $proto = shift; my $class = ref($proto) || $proto; - my %arg = @_; # hash of arguments + my %arg; + if (scalar(@_) == 1 && ref($_[0]) eq 'UPS::Nut::AuthConf') { + my $ac = shift; + %arg = $ac->to_nut_args(); + } else { + %arg = @_; # hash of arguments + } my $self = {}; # _initialize will fill it later bless $self, $class; unless ($self->_initialize(%arg)) { # can't initialize @@ -1498,6 +1504,327 @@ This module is distributed under the same license as Perl itself. =cut +package UPS::Nut::AuthConf; + +my @authconf_list; +my $global_defaults_ref; + +sub getAuthConfList { + return @authconf_list; +} + +sub freeAuthConfList { + @authconf_list = (); + $global_defaults_ref = undef; +} + +sub new { + my $class = shift; + my $section = shift || ""; + my $self = { + section => $section, + user => undef, + pass => undef, + certpath => undef, + certfile => undef, + certident => undef, + certpasswd => undef, + ssl_backend => undef, + certhost => undef, + certverify => -1, + forcessl => -1, + }; + bless $self, $class; + return $self; +} + +sub readAuthConfFile { + my $class = shift; + my $filename = shift; + my $fatal_errors = shift || 0; + my $global_scope = shift; + $global_scope = 1 if !defined $global_scope; + + if (!defined $filename) { + my $s; + if (($s = $ENV{NUT_AUTHCONF_FILE}) && -r $s) { + $filename = $s; + } elsif (($s = $ENV{NUT_AUTHCONF_PATH}) && -r "$s/nutauth.conf") { + $filename = "$s/nutauth.conf"; + } elsif ($s = $ENV{HOME}) { + if (-r "$s/.config/nut/nutauth.conf") { + $filename = "$s/.config/nut/nutauth.conf"; + } elsif (-r "$s/.nutauth.conf") { + $filename = "$s/.nutauth.conf"; + } + } + + if (!defined $filename) { + my $confpath = $ENV{NUT_CONFPATH} || "@CONFPATH@"; + if (-r "$confpath/nutauth.conf") { + $filename = "$confpath/nutauth.conf"; + } + } + } + + if (!defined $filename || !-e $filename) { + if ($fatal_errors) { + die "Can't open a user/site-provided default nutauth.conf file" if !defined $filename; + die "Could not open $filename"; + } + return (); + } + + my $fh; + if (!open($fh, '<', $filename)) { + die "Error opening $filename: $!" if $fatal_errors; + return (); + } + + my $current_ac; + # If we are in global scope (no section yet), we might be re-populating global_defaults + $current_ac = $global_defaults_ref if $global_scope; + + while (my $line = <$fh>) { + chomp $line; + $line =~ s/^\s+|\s+$//g; + next if !$line || $line =~ /^#/; + + if ($line =~ /^\[(.*)\]$/) { + my $sectname = $1; + if ($sectname eq "_global_defaults" || $sectname eq "") { + if (!defined $global_defaults_ref) { + $global_defaults_ref = UPS::Nut::AuthConf->new(""); + push @authconf_list, $global_defaults_ref; + } + $current_ac = $global_defaults_ref; + } else { + # Check if section already exists + $current_ac = undef; + foreach my $ac (@authconf_list) { + my $ac_sect = $ac->{section}; + $ac_sect =~ s/^\[//; + $ac_sect =~ s/\]$//; + if ($ac_sect eq $sectname) { + $current_ac = $ac; + last; + } + } + if (!defined $current_ac) { + $current_ac = UPS::Nut::AuthConf->new("[$sectname]"); + push @authconf_list, $current_ac; + } + } + next; + } + + # INCLUDE support + if ($line =~ /^INCLUDE(?:_REQUIRED)?\s+(.*)$/i) { + my $inc_file = $1; + $inc_file =~ s/^["']|["']$//g; + my $is_required = ($line =~ /^INCLUDE_REQUIRED/i); + $class->readAuthConfFile($inc_file, $is_required, (!defined $current_ac || $current_ac == $global_defaults_ref)); + next; + } + + if ($line =~ /^([^=]+)=(.*)$/) { + my ($key, $value) = ($1, $2); + $key =~ s/^\s+|\s+$//g; + $key = lc($key); + $value =~ s/^\s+|\s+$//g; + $value =~ s/^["']|["']$//g; + + if (!defined $current_ac) { + if ($global_scope) { + if (!defined $global_defaults_ref) { + $global_defaults_ref = UPS::Nut::AuthConf->new(""); + push @authconf_list, $global_defaults_ref; + } + $current_ac = $global_defaults_ref; + } else { + next; + } + } + + if ($key eq 'user') { $current_ac->{user} = $value; } + elsif ($key eq 'password') { $current_ac->{pass} = $value; } + elsif ($key eq 'certpath') { $current_ac->{certpath} = $value; } + elsif ($key eq 'certfile') { $current_ac->{certfile} = $value; } + elsif ($key eq 'certident') { $current_ac->{certident} = $value; } + elsif ($key eq 'certpasswd') { $current_ac->{certpasswd} = $value; } + elsif ($key eq 'ssl_backend') { $current_ac->{ssl_backend} = $value; } + elsif ($key eq 'certhost') { $current_ac->{certhost} = $value; } + elsif ($key eq 'certverify') { $current_ac->{certverify} = ($value =~ /^(on|yes|1)$/i) ? 1 : 0; } + elsif ($key eq 'forcessl') { $current_ac->{forcessl} = ($value =~ /^(on|yes|1)$/i) ? 1 : 0; } + } + } + close($fh); + return @authconf_list; +} + +sub merge { + my ($self, $source) = @_; + return if !defined $source; + + my $sect = $self->{section}; + $sect =~ s/^\[//; + $sect =~ s/\]$//; + + if ($sect =~ /^([^@]+)@/) { + $self->{user} = $1 if !defined $self->{user}; + } else { + $self->{user} = $source->{user} if !defined $self->{user} && defined $source->{user}; + } + + $self->{pass} = $source->{pass} if !defined $self->{pass} && defined $source->{pass}; + $self->{certpath} = $source->{certpath} if !defined $self->{certpath} && defined $source->{certpath}; + $self->{certfile} = $source->{certfile} if !defined $self->{certfile} && defined $source->{certfile}; + $self->{certident} = $source->{certident} if !defined $self->{certident} && defined $source->{certident}; + $self->{certpasswd} = $source->{certpasswd} if !defined $self->{certpasswd} && defined $source->{certpasswd}; + $self->{ssl_backend} = $source->{ssl_backend} if !defined $self->{ssl_backend} && defined $source->{ssl_backend}; + $self->{certhost} = $source->{certhost} if !defined $self->{certhost} && defined $source->{certhost}; + $self->{certverify} = $source->{certverify} if $self->{certverify} == -1 && $source->{certverify} != -1; + $self->{forcessl} = $source->{forcessl} if $self->{forcessl} == -1 && $source->{forcessl} != -1; +} + +sub getAuthConf { + my $class = shift; + my ($user, $host, $port, $auth_configs_ref) = @_; + my @auth_configs = $auth_configs_ref ? @$auth_configs_ref : ( @authconf_list ? @authconf_list : $class->readAuthConfFile() ); + + my $norm_host = $host || 'localhost'; + my $norm_port = $port; # may be undef + + my $host_part = $norm_host; + if (($host_part =~ /:/ || $host_part =~ /\[/ || $host_part =~ /\]/) && substr($host_part, 0, 1) ne '[') { + $host_part = "[$host_part]"; + } + + my $target_user_host_port = ""; + $target_user_host_port .= "$user\@" if defined $user; + $target_user_host_port .= $host_part; + $target_user_host_port .= ":$norm_port" if defined $norm_port; + + my $target_host_port = "\@$host_part"; + $target_host_port .= ":$norm_port" if defined $norm_port; + + my $res = UPS::Nut::AuthConf->new("[$target_user_host_port]"); + + my ($retval_user, $retval_host, $global_defaults); + + foreach my $ac (@auth_configs) { + my $sect = $ac->{section}; + $sect =~ s/^\[//; + $sect =~ s/\]$//; + + if ($sect eq $target_user_host_port) { + $retval_user = $ac; + } + if ($sect eq $target_host_port) { + $retval_host = $ac; + } + if ($sect eq "" || $sect eq "_global_defaults") { + $global_defaults = $ac; + } + } + + $res->merge($retval_user) if defined $retval_user; + $res->merge($retval_host) if defined $retval_host; + $res->merge($global_defaults) if defined $global_defaults; + + # Final enforcement of user if requested + $res->{user} = $user if defined $user; + + return $res; +} + +sub findAuthConf { + my $class = shift; + my ($user, $host, $port, $auth_configs_ref) = @_; + my @auth_configs = $auth_configs_ref ? @$auth_configs_ref : ( @authconf_list ? @authconf_list : $class->readAuthConfFile() ); + + my $norm_host = $host || 'localhost'; + my $norm_port = $port; # may be undef + + my $host_part = $norm_host; + if (($host_part =~ /:/ || $host_part =~ /\[/ || $host_part =~ /\]/) && substr($host_part, 0, 1) ne '[') { + $host_part = "[$host_part]"; + } + + my $star_match; + foreach my $ac (@auth_configs) { + my $section = $ac->{section}; + $section =~ s/^\[//; + $section =~ s/\]$//; + + my $target = ""; + $target .= "$user\@" if defined $user; + $target .= $host_part; + $target .= ":$norm_port" if defined $norm_port; + + return $ac if $section eq $target; + + # fallback matches + if (!defined $user && defined $norm_port && $section eq "$host_part:$norm_port") { + return $ac; + } + if (!defined $user && !defined $norm_port && $section eq $host_part) { + return $ac; + } + + $star_match = $ac if $section eq '*'; + } + + return $star_match if defined $star_match; + return undef; +} + +sub to_nut_args { + my $self = shift; + my %args; + + # Extract HOST and PORT from section + my $host = 'localhost'; + my $port = '3493'; + my $sect = $self->{section}; + $sect =~ s/^\[//; + $sect =~ s/\]$//; + if ($sect =~ /@/) { + $sect =~ s/^[^@]*@//; + } + + if (substr($sect, 0, 1) eq '[') { + my $close = index($sect, ']'); + if ($close != -1) { + $host = substr($sect, 1, $close - 1); + if (length($sect) > $close + 1 && substr($sect, $close + 1, 1) eq ':') { + $port = substr($sect, $close + 2); + } + } else { + $host = $sect; + } + } elsif ($sect =~ /:/) { + ($host, $port) = split(/:/, $sect, 2); + } elsif ($sect ne "") { + $host = $sect; + } + + $args{HOST} = $host; + $args{PORT} = $port; + $args{USERNAME} = $self->{user} if defined $self->{user}; + $args{PASSWORD} = $self->{pass} if defined $self->{pass}; + $args{LOGIN} = 1 if defined $self->{user}; + $args{USESSL} = 1 if $self->{forcessl} > 0; + $args{FORCESSL} = 1 if $self->{forcessl} > 0; + $args{CERTVERIFY} = ($self->{certverify} > 0) if $self->{certverify} != -1; + $args{CERTPATH} = $self->{certpath} if defined $self->{certpath}; + $args{CERTFILE} = $self->{certfile} if defined $self->{certfile}; + $args{CERTIDENT} = $self->{certident} if defined $self->{certident}; + $args{CERTPASSWD} = $self->{certpasswd} if defined $self->{certpasswd}; + + return %args; +} + package UPS::Nut::TrackingID; use strict; diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 4790f36cec..7f38b0d5b5 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -72,6 +72,8 @@ import socket import time import re +import os +import getpass ssl_available = False try: @@ -111,6 +113,272 @@ class TrackingID: def isValid(self): return self.__id is not None and self.__id != "" +class AuthConf: + """ Authentication configuration for a NUT client. """ + __authconf_list = [] + __global_defaults = None + + @staticmethod + def getAuthConfList(): + """ Get the one global list of all parsed authentication configurations """ + return AuthConf.__authconf_list + + @staticmethod + def freeAuthConfList(): + """ Clear the global list of authentication configurations """ + AuthConf.__authconf_list = [] + AuthConf.__global_defaults = None + + def __init__(self, section=""): + self.section = section + self.user = None + self.pass_ = None + self.certpath = None + self.certfile = None + self.certident = None + self.certpasswd = None + self.ssl_backend = None + self.certhost = None + self.certverify = -1 + self.forcessl = -1 + + @staticmethod + def readAuthConfFile(filename=None, fatal_errors=False, global_scope=True): + """ Read the authentication configuration file (usually nutauth.conf) """ + if filename is None: + # Matches C implementation logic for default path + s = os.environ.get("NUT_AUTHCONF_FILE") + if s and os.path.exists(s): + filename = s + else: + s = os.environ.get("NUT_AUTHCONF_PATH") + if s and os.path.exists(os.path.join(s, "nutauth.conf")): + filename = os.path.join(s, "nutauth.conf") + else: + s = os.environ.get("HOME") + if s: + p = os.path.join(s, ".config", "nut", "nutauth.conf") + if os.path.exists(p): + filename = p + else: + p = os.path.join(s, ".nutauth.conf") + if os.path.exists(p): + filename = p + + if filename is None: + filename = os.path.join(os.environ.get("NUT_CONFPATH", "@CONFPATH@"), "nutauth.conf") + + if not os.path.exists(filename): + if fatal_errors: + raise PyNUTError(f"Can't open a user/site-provided default nutauth.conf file" if filename is None else f"Could not open {filename}") + return AuthConf.__authconf_list + + current_ac = AuthConf.__global_defaults if global_scope else None + + try: + with open(filename, 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + + if line.startswith('[') and line.endswith(']'): + sectname = line[1:-1] + if sectname in ("_global_defaults", ""): + if AuthConf.__global_defaults is None: + AuthConf.__global_defaults = AuthConf("") + AuthConf.__authconf_list.append(AuthConf.__global_defaults) + current_ac = AuthConf.__global_defaults + else: + # Check if section already exists + current_ac = None + for ac in AuthConf.__authconf_list: + ac_sect = ac.section + if ac_sect.startswith('[') and ac_sect.endswith(']'): + ac_sect = ac_sect[1:-1] + if ac_sect == sectname: + current_ac = ac + break + if current_ac is None: + current_ac = AuthConf(line) + AuthConf.__authconf_list.append(current_ac) + continue + + # INCLUDE support + m = re.match(r'^(INCLUDE(?:_REQUIRED)?)\s+(.*)$', line, re.I) + if m: + inc_type = m.group(1).upper() + inc_file = m.group(2).strip() + if (inc_file.startswith('"') and inc_file.endswith('"')) or \ + (inc_file.startswith("'") and inc_file.endswith("'")): + inc_file = inc_file[1:-1] + + is_required = (inc_type == "INCLUDE_REQUIRED") + AuthConf.readAuthConfFile(inc_file, is_required, (current_ac is None or current_ac == AuthConf.__global_defaults)) + continue + + if '=' in line: + key, value = line.split('=', 1) + key = key.strip().lower() + value = value.strip() + # Remove quotes if present + if (value.startswith('"') and value.endswith('"')) or \ + (value.startswith("'") and value.endswith("'")): + value = value[1:-1] + + if current_ac is None: + if global_scope: + if AuthConf.__global_defaults is None: + AuthConf.__global_defaults = AuthConf("") + AuthConf.__authconf_list.append(AuthConf.__global_defaults) + current_ac = AuthConf.__global_defaults + else: + continue + + if key == 'user': + current_ac.user = value + elif key == 'password': + current_ac.pass_ = value + elif key == 'certpath': + current_ac.certpath = value + elif key == 'certfile': + current_ac.certfile = value + elif key == 'certident': + current_ac.certident = value + elif key == 'certpasswd': + current_ac.certpasswd = value + elif key == 'ssl_backend': + current_ac.ssl_backend = value + elif key == 'certhost': + current_ac.certhost = value + elif key == 'certverify': + current_ac.certverify = 1 if value.lower() in ('on', 'yes', '1') else 0 + elif key == 'forcessl': + current_ac.forcessl = 1 if value.lower() in ('on', 'yes', '1') else 0 + except Exception as e: + if fatal_errors: + raise PyNUTError(f"Error reading {filename}: {str(e)}") + + return AuthConf.__authconf_list + + def merge(self, source): + """ Merge contents of another configuration item into this one. """ + if source is None: + return + + sect = self.section + if sect.startswith('[') and sect.endswith(']'): + sect = sect[1:-1] + + if '@' in sect and sect.find('@') > 0: + self.user = sect.split('@')[0] + else: + if self.user is None and source.user is not None: + self.user = source.user + + if self.pass_ is None and source.pass_ is not None: + self.pass_ = source.pass_ + if self.certpath is None and source.certpath is not None: + self.certpath = source.certpath + if self.certfile is None and source.certfile is not None: + self.certfile = source.certfile + if self.certident is None and source.certident is not None: + self.certident = source.certident + if self.certpasswd is None and source.certpasswd is not None: + self.certpasswd = source.certpasswd + if self.ssl_backend is None and source.ssl_backend is not None: + self.ssl_backend = source.ssl_backend + if self.certhost is None and source.certhost is not None: + self.certhost = source.certhost + if self.certverify == -1 and source.certverify != -1: + self.certverify = source.certverify + if self.forcessl == -1 and source.forcessl != -1: + self.forcessl = source.forcessl + + @staticmethod + def getAuthConf(user=None, host=None, port=None, auth_configs=None): + """ Find the best matching authconf for a given connection string, and fill in + the missing points from higher levels (exact match => host defaults => global). + """ + if auth_configs is None: + auth_configs = AuthConf.__authconf_list if AuthConf.__authconf_list else AuthConf.readAuthConfFile() + + norm_host = host if host else 'localhost' + norm_port = port # may be None + + # Proper IPv6 bracket handling for construction + host_part = norm_host + if (':' in host_part or '[' in host_part or ']' in host_part) and not host_part.startswith('['): + host_part = f"[{host_part}]" + + target_user_host_port = "" + if user: target_user_host_port += f"{user}@" + target_user_host_port += f"{host_part}" + if norm_port: target_user_host_port += f":{norm_port}" + + target_host_port = f"@{host_part}" + if norm_port: target_host_port += f":{norm_port}" + + res = AuthConf(f"[{target_user_host_port}]") + + retval_user = None + retval_host = None + + for ac in auth_configs: + sect = ac.section[1:-1] + if sect == target_user_host_port: + retval_user = ac + elif sect == target_host_port: + retval_host = ac + + if retval_user: res.merge(retval_user) + if retval_host: res.merge(retval_host) + if AuthConf.__global_defaults: res.merge(AuthConf.__global_defaults) + + # Final enforcement of user if requested + if user: + res.user = user + + return res + + @staticmethod + def findAuthConf(user=None, host=None, port=None, auth_configs=None): + """ Find the best matching authconf for a given connection string """ + if auth_configs is None: + auth_configs = AuthConf.__authconf_list if AuthConf.__authconf_list else AuthConf.readAuthConfFile() + + norm_host = host if host else 'localhost' + norm_port = port # may be None + + # Proper IPv6 bracket handling for construction + host_part = norm_host + if (':' in host_part or '[' in host_part or ']' in host_part) and not host_part.startswith('['): + host_part = f"[{host_part}]" + + # Try to match [user@host:port], then [host:port], then [host], then [*] + # This is a simplified version of the C logic + for ac in auth_configs: + section = ac.section[1:-1] # Remove brackets + + # 1. Exact match user@host:port + target = "" + if user: target += f"{user}@" + target += f"{host_part}" + if norm_port: target += f":{norm_port}" + + if section == target: + return ac + + # 2. Match host:port + if not user and section == f"{host_part}:{norm_port}": + return ac + + # 3. Match host + if not user and not norm_port and section == host_part: + return ac + + return AuthConf.__global_defaults + class PyNUTError( Exception ) : """ Base class for custom exceptions """ @@ -118,6 +386,69 @@ class PyNUTError( Exception ) : class PyNUTClient : """ Abstraction class to access NUT (Network UPS Tools) server """ + @classmethod + def from_authconf(cls, authconf, **kwargs): + """ Create a PyNUTClient from an AuthConf object """ + # Extract host and port from authconf.section if possible + host = "127.0.0.1" + port = 3493 + + # Simple split, could be more robust + section = authconf.section + if section.startswith('['): + section = section[1:-1] + + # Handle user@host:port + if '@' in section: + user_part, addr_part = section.split('@', 1) + else: + addr_part = section + + if addr_part: + if addr_part.startswith('['): + close_bracket = addr_part.find(']') + if close_bracket != -1: + host = addr_part[1:close_bracket] + if len(addr_part) > close_bracket + 1 and addr_part[close_bracket+1] == ':': + try: + port = int(addr_part[close_bracket+2:]) + except ValueError: + pass + else: + pass # port remains default + else: + host = addr_part # Should not happen in well-formed authconf + elif ':' in addr_part: + parts = addr_part.split(':', 1) + host = parts[0] + try: + port = int(parts[1]) + except ValueError: + pass + else: + host = addr_part + + if host == "": + host = "127.0.0.1" + + client_args = { + "host": host, + "port": port, + "login": authconf.user, + "password": authconf.pass_, + "use_ssl": authconf.forcessl > 0, + "force_ssl": authconf.forcessl > 0, + "cert_verify": authconf.certverify > 0 if authconf.certverify != -1 else None, + "ca_path": authconf.certpath, + "cert_file": authconf.certfile, + "cert_ident": authconf.certident, + "key_pass": authconf.certpasswd, + "certhost": authconf.certhost + } + # Override with any explicit kwargs + client_args.update(kwargs) + return cls(**client_args) + __debug = None # Set class to debug mode (prints everything useful for debugging...) __host = None __port = None @@ -138,7 +469,8 @@ class PyNUTClient : except: NUT_DEFAULT_CONNECT_TIMEOUT = 5.0 - def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, tracking=False, + def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, + connect_now=True, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, tracking=False, use_ssl=False, ssl_context=None, cert_verify=None, ca_file=None, ca_path=None, cert_file=None, key_file=None, key_pass=None, force_ssl=False, cert_ident=None, certhost=None ) : """ Class initialization method @@ -148,8 +480,9 @@ port : Port where NUT listens for connections (default to 3493) login : Login used to connect to NUT server (default to None for no authentication) password : Password used when using authentication (default to None) debug : Boolean, put class in debug mode (prints everything on console, default to False) +connect_now : Boolean, connect to NUT server immediately and apply "tracking" setting (default to True) timeout : Timeout used to wait for network response -tracking : Boolean or 'ON'/'OFF', try to enable TRACKING support right away +tracking : Boolean or 'ON'/'OFF', if we connect and try to enable TRACKING support right away (remembered for subsequent connect() call if not connecting now) use_ssl : Boolean, use SSL/TLS for connection (default to False; subject to 'ssl' module availability) ssl_context : ssl.SSLContext object to use for SSL/TLS connection (default to None; subject to 'ssl' module availability) cert_verify : Boolean, verify server certificate (default to None, which means True if CA info is provided) @@ -281,9 +614,13 @@ certhost : Optional dict or list of dict/lists: {hostname: [certname, certver if force_ssl: raise PyNUTError( "SSL required but 'ssl' module not available" ) - self.__connect() + self.__tracking_wanted = tracking + self.__tracking = None + ''' Actual tracking state applied over protocol (maybe) ''' - self.__tracking = self.SetTrackingMode(tracking) + if connect_now: + # Sets self.__tracking if explicit mode is wanted + self.__connect() # Try to disconnect cleanly when class is deleted ;) def __del__( self ) : @@ -399,6 +736,8 @@ if something goes wrong. else: raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + self.__tracking = self.SetTrackingMode(self.__tracking_wanted) + def SetTrackingMode( self, value="" ) : """ Enable/disable TRACKING ability for SETVAR/INSTCMD ('ON'/'OFF') diff --git a/scripts/python/module/test_nutclient.py.in b/scripts/python/module/test_nutclient.py.in index 701f09c853..de26abd924 100755 --- a/scripts/python/module/test_nutclient.py.in +++ b/scripts/python/module/test_nutclient.py.in @@ -7,6 +7,38 @@ import PyNUT import sys import os + +def test_splitaddr(addr, expected_host, expected_port): + ac = PyNUT.AuthConf(f"[{addr}]") + client = PyNUT.PyNUTClient.from_authconf(ac, connect_now=False) + # Note: host in PyNUTClient is __host, but we can't access it easily if it's private. + # Looking at the code, we might need to add a getter or use _PyNUTClient__host + host = client._PyNUTClient__host + port = client._PyNUTClient__port + + if host == expected_host and port == expected_port: + print(f"OK: {addr} -> {host}:{port}") + else: + print(f"FAIL: {addr} -> expected {expected_host}:{expected_port}, got {host}:{port}") + return 1 + return 0 + +def testsuite_splitaddr(): + errors = 0 + errors += test_splitaddr("localhost", "localhost", 3493) + errors += test_splitaddr("localhost:12345", "localhost", 12345) + errors += test_splitaddr("[::1]", "::1", 3493) + errors += test_splitaddr("[::1]:12345", "::1", 12345) + errors += test_splitaddr("[fe80::215:5dff:fea4:f780]", "fe80::215:5dff:fea4:f780", 3493) + errors += test_splitaddr("[fe80::215:5dff:fea4:f780]:3495", "fe80::215:5dff:fea4:f780", 3495) + + if errors: + print("Some Python splitaddr tests FAILED") + return False + + print("All Python splitaddr tests passed!") + return True + if __name__ == "__main__" : NUT_HOST = os.getenv('NUT_HOST', '127.0.0.1') NUT_PORT = int(os.getenv('NUT_PORT', '3493')) @@ -193,6 +225,10 @@ if __name__ == "__main__" : failed.append('ListClients-dummy-after') print( "\033[01;33m%s\033[0m\n" % result ) + print( 80*"-" + "\nTesting authconf section name parser" ) + if not testsuite_splitaddr(): + failed.append('testsuite_splitaddr') + print( 80*"-" + "\nTesting 'PyNUT' instance teardown (end of test script)" ) # No more tests AFTER this line; add them above the teardown message diff --git a/server/netssl.c b/server/netssl.c index e7026c96e4..0a939c2836 100644 --- a/server/netssl.c +++ b/server/netssl.c @@ -1107,7 +1107,7 @@ void ssl_init(void) if (!ssl_ctx) { ssl_debug(); - fatalx(EXIT_FAILURE, "SSL_CTX_new failed"); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: SSL_CTX_new failed"); } SSL_CTX_set_options(ssl_ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); @@ -1124,7 +1124,7 @@ void ssl_init(void) # else /* newer OPENSSL_VERSION_NUMBER */ if (SSL_CTX_set_min_proto_version(ssl_ctx, disable_weak_ssl ? TLS1_2_VERSION : TLS1_VERSION) != 1) { ssl_debug(); - fatalx(EXIT_FAILURE, "SSL_CTX_set_min_proto_version(TLS1_VERSION)"); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: SSL_CTX_set_min_proto_version(TLS1_VERSION)"); } # endif /* OPENSSL_VERSION_NUMBER */ @@ -1161,25 +1161,24 @@ void ssl_init(void) # else /* Not SSL_* methods either */ - upslogx(LOG_ERR, "Private key password support not implemented for OpenSSL < ~0.9.6..~1.1 yet"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: Private key password support not implemented for OpenSSL < ~0.9.6..~1.1 yet", certfile); # endif # endif /* ...SET_DEFAULT_PASSWD_CB */ } /* else: CERTIDENT did not pass a password, nothing to check */ if (SSL_CTX_use_certificate_chain_file(ssl_ctx, certfile) != 1) { ssl_debug(); - fatalx(EXIT_FAILURE, "SSL_CTX_use_certificate_chain_file(%s) failed", certfile); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: SSL_CTX_use_certificate_chain_file(%s) failed", certfile); } if (SSL_CTX_use_PrivateKey_file(ssl_ctx, certfile, SSL_FILETYPE_PEM) != 1) { ssl_debug(); - fatalx(EXIT_FAILURE, "SSL_CTX_use_PrivateKey_file(%s) failed", certfile); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: SSL_CTX_use_PrivateKey_file(%s) failed", certfile); } if (SSL_CTX_check_private_key(ssl_ctx) != 1) { ssl_debug(); - fatalx(EXIT_FAILURE, "SSL_CTX_check_private_key(%s) failed", certfile); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: SSL_CTX_check_private_key(%s) failed", certfile); } if (certname && certname[0] != '\0') { @@ -1214,7 +1213,7 @@ void ssl_init(void) if (subject) { OPENSSL_free(subject); } - fatalx(EXIT_FAILURE, "Unexpected certificate provided"); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: Unexpected certificate provided"); } else { upsdebugx(2, "Certificate subject verified against CERTIDENT subject name (%s)", certname); } @@ -1223,20 +1222,22 @@ void ssl_init(void) } } # else /* Missing X509 methods wanted above */ - fatalx(EXIT_FAILURE, "CERTIDENT name verification is not supported in this OpenSSL build (too old)"); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: CERTIDENT name verification is not supported in this OpenSSL build (too old)"); # endif /* Got ways to check CERTIDENT? */ - }/* else: CERTIDENT did not pass a name, nothing to check */ + } /* else: CERTIDENT did not pass a name, nothing to check; note that + * with OpenSSL (unlike NSS somewhat) the CERTFILE is asumed to + * reliably specify (suffices to know) what we want to load */ upsdebugx(2, "%s: initialized with OpenSSL and certfile='%s'", __func__, certfile); if (SSL_CTX_set_cipher_list(ssl_ctx, "HIGH:@STRENGTH") != 1) { ssl_debug(); - fatalx(EXIT_FAILURE, "SSL_CTX_set_cipher_list failed"); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: SSL_CTX_set_cipher_list failed"); } # ifdef WITH_CLIENT_CERTIFICATE_VALIDATION if (certrequest < NETSSL_CERTREQ_NO || certrequest > NETSSL_CERTREQ_REQUIRE) { - fatalx(EXIT_FAILURE, "Invalid certificate requirement"); + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize OpenSSL backend: Invalid certificate requirement"); } if (certpath) { @@ -1248,7 +1249,7 @@ void ssl_init(void) if (SSL_CTX_load_verify_locations(ssl_ctx, certpath, NULL) != 1) { ssl_debug(); upslogx(LOG_ERR, "Failed to load CA certificate(s) from directory or file %s", certpath); - /* return? fatal? */ + /* TOTHINK: return? fatal? */ } else { upsdebugx(1, "%s: ...but succeeded to load CA certificate(s) from file %s", __func__, certpath); } @@ -1282,13 +1283,21 @@ void ssl_init(void) SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, NULL); # endif + upsdebugx(2, "%s: initialized with OpenSSL and certpath='%s' / certfile='%s'", + __func__, NUT_STRARG(certpath), NUT_STRARG(certfile)); ssl_initialized = 1; # elif defined(WITH_NSS) /* not WITH_OPENSSL */ if (!certname || certname[0]==0 ) { - upslogx(LOG_ERR, "The SSL certificate name is not specified."); - return; + /* TOTHINK: This is not for CERTIDENT double-check like above, + * but directly which cert we should load. Maybe we can fall + * back to discovering the only cert with a private key, + * if the NSS DB can be opened below?.. + */ + /* SSL setup was specified, but incompletely. + * This start-up is unexpectedly unsafe => abort. */ + fatalx(EXIT_FAILURE, "The SSL certificate name is not specified, required with NSS backend."); } PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); @@ -1301,16 +1310,26 @@ void ssl_init(void) * probably NSS key db object allocation too. */ status = NSS_Init(certpath); if (status != SECSuccess) { + /* NSS DB file error handling is complicated: anything + * unexpected means SEC_ERROR_LEGACY_DATABASE, whether that + * really is an old database, or insufficient permissions, + * or a missing directory we point to... and this mess + * includes the case that we have a set of files with the + * *old* (~2014) NSS library and *NEW* database format in + * the specified directory (cert9.db not cert8.db)! + * Note that more recent libraries (2020's) are okay with + * either of these two formats. + */ upslogx(LOG_ERR, "Can not initialize SSL context"); nss_error("ssl_init / NSS_Init"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } status = NSS_SetDomesticPolicy(); if (status != SECSuccess) { upslogx(LOG_ERR, "Can not initialize SSL policy"); nss_error("ssl_init / NSS_SetDomesticPolicy"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } /* Default server cache config */ @@ -1318,7 +1337,7 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not initialize SSL server cache"); nss_error("ssl_init / SSL_ConfigServerSessionIDCache"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } if (!disable_weak_ssl) { @@ -1326,13 +1345,13 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not enable SSLv3"); nss_error("ssl_init / SSL_OptionSetDefault(SSL_ENABLE_SSL3)"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } status = SSL_OptionSetDefault(SSL_ENABLE_TLS, PR_TRUE); if (status != SECSuccess) { upslogx(LOG_ERR, "Can not enable TLSv1"); nss_error("ssl_init / SSL_OptionSetDefault(SSL_ENABLE_TLS)"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } } else { # if defined(NSS_VMAJOR) && (NSS_VMAJOR > 3 || (NSS_VMAJOR == 3 && defined(NSS_VMINOR) && NSS_VMINOR >= 14)) @@ -1340,7 +1359,7 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not get versions supported"); nss_error("ssl_init / SSL_VersionRangeGetSupported"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } range.min = SSL_LIBRARY_VERSION_TLS_1_1; # ifdef SSL_LIBRARY_VERSION_TLS_1_2 @@ -1350,7 +1369,7 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not set versions supported"); nss_error("ssl_init / SSL_VersionRangeSetDefault"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } /* Disable old/weak ciphers */ SSL_CipherPrefSetDefault(TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA, PR_FALSE); @@ -1362,13 +1381,13 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not disable SSLv3"); nss_error("ssl_init / SSL_OptionSetDefault(SSL_DISABLE_SSL3)"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } status = SSL_OptionSetDefault(SSL_ENABLE_TLS, PR_TRUE); if (status != SECSuccess) { upslogx(LOG_ERR, "Can not enable TLSv1"); nss_error("ssl_init / SSL_OptionSetDefault(SSL_ENABLE_TLS)"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } # endif /* NSS_VMAJOR */ } @@ -1378,8 +1397,7 @@ void ssl_init(void) || certrequest > NETSSL_CERTREQ_REQUIRE /* > 2 */ ) { upslogx(LOG_ERR, "Invalid certificate requirement"); - return; - } + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); if (certrequest == NETSSL_CERTREQ_REQUEST /* 1 */ || certrequest == NETSSL_CERTREQ_REQUIRE /* 2 */ @@ -1388,7 +1406,7 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not enable certificate request"); nss_error("ssl_init / SSL_OptionSetDefault(SSL_REQUEST_CERTIFICATE)"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } } @@ -1397,30 +1415,32 @@ void ssl_init(void) if (status != SECSuccess) { upslogx(LOG_ERR, "Can not enable certificate requirement"); nss_error("ssl_init / SSL_OptionSetDefault(SSL_REQUIRE_CERTIFICATE)"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } } # endif /* WITH_CLIENT_CERTIFICATE_VALIDATION */ + /* TOTHINK: if certname is not specified (see also the check above) + * fall back to finding the (only?) cert with a private key? */ cert = PK11_FindCertFromNickname(certname, NULL); if (cert == NULL) { upslogx(LOG_ERR, "Can not find server certificate"); nss_error("ssl_init / PK11_FindCertFromNickname"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } privKey = PK11_FindKeyByAnyCert(cert, NULL); if (privKey == NULL) { upslogx(LOG_ERR, "Can not find private key associate to server certificate"); nss_error("ssl_init / PK11_FindKeyByAnyCert"); - return; + fatalx(EXIT_FAILURE, "SSL configuration was specified, but NUT server failed to initialize NSS backend."); } upsdebugx(2, "%s: initialized with NSS and certpath='%s'", __func__, certpath); ssl_initialized = 1; # else /* not (WITH_OPENSSL | WITH_NSS) */ /* Looking at ifdefs, we should not get here. But just in case... */ - upslogx(LOG_ERR, "ssl_init called but no supported SSL backend wasn compiled in"); + upslogx(LOG_ERR, "ssl_init called but no supported SSL backend was compiled in"); # endif /* WITH_OPENSSL | WITH_NSS */ } 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/.gitignore b/tests/.gitignore index 59edbae1af..f1dc2fabb0 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -11,6 +11,7 @@ /gpiotest.log /gpiotest.trs /test-suite.log +/test_authconf /selftest-rw/* /nutlogtest-nofail.sh /nutlogtest 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 2836dc1bd9..d17ae02a98 100644 --- a/tests/NIT/Makefile.am +++ b/tests/NIT/Makefile.am @@ -63,6 +63,8 @@ check-NIT-devel: $(abs_srcdir)/nit.sh @dotMAKE@ +@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)/scripts" && $(MAKE) $(AM_MAKEFLAGS) -s perl/UPS/Nut.pm + +@cd "$(top_builddir)/scripts/python/module/" && $(MAKE) $(AM_MAKEFLAGS) -s PyNUT.py test_nutclient.py +@cd "$(top_builddir)" && $(MAKE) $(AM_MAKEFLAGS) -s cleanup-touchfiles-for-generated-headers +@$(MAKE) $(AM_MAKEFLAGS) check-NIT diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 97c66ed42f..f56fda812d 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -231,7 +231,7 @@ isBusy_NUT_PORT() { } die() { - echo "[FATAL] $@" >&2 + echo "`TZ=UTC LANG=C date` [FATAL] $@" >&2 exit 1 } @@ -536,32 +536,6 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in ;; esac -case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in - *NSS*) - (command -v certutil) || { - if [ x"${WITH_SSL_TESTS}" = xrequired-conditional ] ; then - die "Aborting because SSL tests are required, but needed third-party tooling was not found to produce the crypto credential stores for NSS" - fi - log_warn "NUT can use NSS, but needed third-party tooling was not found to produce the crypto credential stores" - if [ x"${WITH_SSL_CLIENT}" = xNSS ] ; then WITH_SSL_CLIENT="none" ; fi - if [ x"${WITH_SSL_SERVER}" = xNSS ] ; then WITH_SSL_SERVER="none" ; fi - } - ;; -esac - -case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in - *OpenSSL*) - (command -v openssl) || { - if [ x"${WITH_SSL_TESTS}" = xrequired-conditional ] ; then - die "Aborting because SSL tests are required, but needed third-party tooling was not found to produce the crypto credential stores for OpenSSL" - fi - log_warn "NUT can use OpenSSL, but needed third-party tooling was not found to produce the crypto credential stores" - if [ x"${WITH_SSL_CLIENT}" = xOpenSSL ] ; then WITH_SSL_CLIENT="none" ; fi - if [ x"${WITH_SSL_SERVER}" = xOpenSSL ] ; then WITH_SSL_SERVER="none" ; fi - } - ;; -esac - TESTCERT_ROOTCA_NAME="NUT Mock Root CA" TESTCERT_ROOTCA_PASS="VeryS@cur@1337" TESTCERT_CLIENT_NAME="NIT upsmon" @@ -841,7 +815,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?" @@ -901,6 +879,8 @@ log_info "Using NUT_PORT=${NUT_PORT} for this test run" # Adjust path spelling to run-time platform, libraries seem to want that on WIN32 # NOTE: Windows backslashes are pre-escaped in the configure-generated value +# NOTE: For mingw bash at least, shell globs (wildcards, not exact file names) +# should be separated by a forward slash in any case. case "${ABS_TOP_BUILDDIR}" in ?":\\"*) TESTCERT_PATH_SEP='\\' @@ -977,25 +957,26 @@ check_NIT_certs_NSS() { log_info "=== Verifying NSS ${1} DB files:" ( # Older: cert8.db key3.db secmod.db - if [ -e "${2}/${3}cert8.db" ] ; then - ls -l "${2}/${3}cert8.db" "${2}/${3}key3.db" "${2}/${3}secmod.db" || exit + if [ -e "${2}${TESTCERT_PATH_SEP}${3}cert8.db" ] ; then + ls -l "${2}${TESTCERT_PATH_SEP}${3}cert8.db" "${2}${TESTCERT_PATH_SEP}${3}key3.db" "${2}${TESTCERT_PATH_SEP}${3}secmod.db" || exit for F in cert8.db key3.db secmod.db ; do - test -s "${2}/${3}${F}" || die "File '${2}/${3}${F}' is empty" + test -s "${2}${TESTCERT_PATH_SEP}${3}${F}" || die "File '${2}${TESTCERT_PATH_SEP}${3}${F}' is empty" done exit 0 fi # Newer: cert9.db key4.db pkcs11.txt - if [ -e "${2}/${3}cert9.db" ] ; then - ls -l "${2}/${3}cert9.db" "${2}/${3}key4.db" "${2}/${3}pkcs11.txt" || exit + if [ -e "${2}${TESTCERT_PATH_SEP}${3}cert9.db" ] ; then + ls -l "${2}${TESTCERT_PATH_SEP}${3}cert9.db" "${2}${TESTCERT_PATH_SEP}${3}key4.db" "${2}${TESTCERT_PATH_SEP}${3}pkcs11.txt" || exit for F in cert9.db key4.db pkcs11.txt ; do - test -s "${2}/${3}${F}" || die "File '${2}/${3}${F}' is empty" + test -s "${2}${TESTCERT_PATH_SEP}${3}${F}" || die "File '${2}${TESTCERT_PATH_SEP}${3}${F}' is empty" done exit 0 fi - ls -l "${2}/${3}"*.txt || true - ls -l "${2}/${3}"*.db || exit + # See comments above about no TESTCERT_PATH_SEP for shell globs. + ls -l "${2}"/"${3}"*.txt || true + ls -l "${2}"/"${3}"*.db || exit ) || die "Could not list NSS ${1} DB files" # NSS certutil error handling is complicated: anything unexpected means @@ -1014,19 +995,23 @@ check_NIT_certs_NSS() { # TBD: Consider `-P "$3"` prefix eventually OUT="`certutil -d \"$2\" -L 2>&1`" if [ "$?" != 0 ] ; then - if echo "$OUT" | ${EGREP} 'SEC_ERROR_LEGACY_DATABASE' && [ -e "${2}/${3}cert9.db" ] && [ ! -e "${2}/${3}cert8.db" ] ; then + if echo "$OUT" | ${EGREP} 'SEC_ERROR_LEGACY_DATABASE' \ + && [ -e "${2}${TESTCERT_PATH_SEP}${3}cert9.db" ] \ + && [ ! -e "${2}${TESTCERT_PATH_SEP}${3}cert8.db" ] \ + ; then log_warn "NSS tools on this worker need old DB format files, but we only have new ones" - if ls -l "${2}"/*.p12 "${2}"/.pwfile && (command -v pk12util) ; then + # See comments above about no TESTCERT_PATH_SEP for shell globs. + if ls -l "${2}"/*.p12 "${2}${TESTCERT_PATH_SEP}".pwfile && (command -v pk12util) ; then log_info "Will try to create older-format DB from P12 files" else # Assume we do not have tools for new NSS DB format files, # use PEM and generate P12 instead: (command -v pk12util) && (command -v openssl) && \ case "$1" in - CA) ls -l "${2}"/*.crt || true ; ls -l "${2}"/*.pem "${2}"/*.key "${2}"/.pwfile ;; + CA) ls -l "${2}"/*.crt || true ; ls -l "${2}"/*.pem "${2}"/*.key "${2}${TESTCERT_PATH_SEP}".pwfile ;; Server|Client) - ls -l "${2}"/*.pem || true ; ls -l "${2}"/*.crt "${2}"/*.key "${2}"/.pwfile ;; + ls -l "${2}"/*.pem || true ; ls -l "${2}"/*.crt "${2}"/*.key "${2}${TESTCERT_PATH_SEP}".pwfile ;; *) die "Unexpected cert store type, no idea how to fix that one: '$1'" ;; esac || die "Can not recreate NSS DB from PEM files: some of them are missing" @@ -1036,16 +1021,16 @@ check_NIT_certs_NSS() { die "Can not recreate NSS DB from PEM files: code transplant needed, but is incomplete." fi - certutil -N -d "${2}" -f "${2}/.pwfile" \ + certutil -N -d "${2}" -f "${2}${TESTCERT_PATH_SEP}.pwfile" \ || die "Could not init NSS $1 database in $2" case "$1" in Server|Client) # Import the CA certificate, so users of this DB trust it: - certutil -A -d "${2}" -f "${2}/.pwfile" \ + certutil -A -d "${2}" -f "${2}${TESTCERT_PATH_SEP}.pwfile" \ -n "${TESTCERT_ROOTCA_NAME}" \ -t "TC,," \ - -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -a -i "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ || die "Could not import the CA certificate to NSS $1 database in $2" ;; esac @@ -1056,12 +1041,13 @@ check_NIT_certs_NSS() { # Import the payload relevant for this directory # Assume one P12 file in the cache dir for this type of info - pk12util -i "${2}"/*.p12 -d "${2}" -k "${2}"/.pwfile -w "${2}"/.pwfile \ + # See comments above about no TESTCERT_PATH_SEP for shell globs. + pk12util -i "${2}"/*.p12 -d "${2}" -k "${2}${TESTCERT_PATH_SEP}".pwfile -w "${2}${TESTCERT_PATH_SEP}".pwfile \ || die "Could not import $1 PKCS#12 to NSS in $2" case "$1" in CA) # Trust it as a CA in NSS DB in the CA directory - certutil -M -d "${2}" -n "${TESTCERT_ROOTCA_NAME}" -t "CT,C,C" -f "${2}/.pwfile" \ + certutil -M -d "${2}" -n "${TESTCERT_ROOTCA_NAME}" -t "CT,C,C" -f "${2}${TESTCERT_PATH_SEP}.pwfile" \ || die "Could not set trust on imported NSS CA" ;; esac @@ -1090,16 +1076,17 @@ check_NIT_certs() { case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in *OpenSSL*) - ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem "${TESTCERT_PATH_ROOTCA}/"*.? \ + # See comments above about no TESTCERT_PATH_SEP for shell globs. + ls -l "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem "${TESTCERT_PATH_ROOTCA}"/*.? \ || die "Could not list OpenSSL CA PEM file and hash links" - ls -l "${TESTCERT_PATH_SERVER}"/upsd.pem \ + ls -l "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"upsd.pem \ || die "Could not list an upsd.pem" - ls -l "${TESTCERT_PATH_CLIENT}"/upsmon.pem \ + ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"upsmon.pem \ || die "Could not list an upsmon.pem" - ls -l "${TESTCERT_PATH_CLIENT}/upsd-public.pem" \ + ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}upsd-public.pem" \ || die "Could not list a upsd-public.pem" ;; esac @@ -1138,19 +1125,21 @@ prepare_NIT_certs() { if [ -n "${TESTCERT_MOCK_PATH-}" ] && [ -d "${TESTCERT_MOCK_PATH}" ]; then log_info "Using provided mock certificates from ${TESTCERT_MOCK_PATH}" # If there is a setup script there, source it to get variables - if [ -f "${TESTCERT_MOCK_PATH}/TESTCERT_VARS.env" ]; then - . "${TESTCERT_MOCK_PATH}/TESTCERT_VARS.env" + if [ -f "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}TESTCERT_VARS.env" ]; then + . "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}TESTCERT_VARS.env" fi # Use them if they exist (note the config might point us to # a new TESTCERT_MOCK_PATH location based on whatever logic, # but at least files generated by this script should not): - if [ -d "${TESTCERT_MOCK_PATH}/rootca" ] \ - && [ -d "${TESTCERT_MOCK_PATH}/upsd" ] \ - && [ -d "${TESTCERT_MOCK_PATH}/upsmon" ] \ + if [ -d "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}rootca" ] \ + && [ -d "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}upsd" ] \ + && [ -d "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}upsmon" ] \ ; then mkdir -p "${TESTCERT_PATH_BASE}" - cp -pr "${TESTCERT_MOCK_PATH}"/* "${TESTCERT_PATH_BASE}/" + # See comments above about no TESTCERT_PATH_SEP for shell globs. + cp -prf "${TESTCERT_MOCK_PATH}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" || \ + cp -prfL "${TESTCERT_MOCK_PATH}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" log_info "Mock certificates deployed from ${TESTCERT_MOCK_PATH}" check_NIT_certs set-none-on-fail || { @@ -1177,7 +1166,7 @@ unset CI_CACHE_NIT_HASHDIR if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] ; then [ -n "${CI_CACHE_NUT_BASEDIR-}" ] || { if [ -n "${HOME-}" ] && [ -d "${HOME}" ] ; then - CI_CACHE_NUT_BASEDIR="${HOME}/.cache/nut-ci" + CI_CACHE_NUT_BASEDIR="${HOME}${TESTCERT_PATH_SEP}.cache${TESTCERT_PATH_SEP}nut-ci" fi } if [ -n "${CI_CACHE_NUT_BASEDIR}" ] ; then @@ -1195,18 +1184,22 @@ if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] ; then else log_info "Found cached NIT certificates in ${CI_CACHE_NIT_HASHDIR}" mkdir -p "${TESTCERT_PATH_BASE}" - cp -pr "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}/" + # See comments above about no TESTCERT_PATH_SEP for shell globs. + cp -prf "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" || \\ + cp -prfL "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" # If there is a setup script there, source it to get variables - if [ -f "${CI_CACHE_NIT_HASHDIR}/TESTCERT_VARS.env" ]; then + if [ -f "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}TESTCERT_VARS.env" ]; then BACKUP_TESTCERT_PATH_BASE="${TESTCERT_PATH_BASE}" - log_info "Sourcing '${CI_CACHE_NIT_HASHDIR}/TESTCERT_VARS.env' ..." - . "${CI_CACHE_NIT_HASHDIR}/TESTCERT_VARS.env" + log_info "Sourcing '${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}TESTCERT_VARS.env' ..." + . "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}TESTCERT_VARS.env" TESTCERT_PATH_BASE="${BACKUP_TESTCERT_PATH_BASE}" fi check_NIT_certs && return - log_warn "FAILED check_NIT_certs with cached data, will generate anew" + + log_warn "FAILED check_NIT_certs with cached data, will generate anew. Removing:" + find "${TESTCERT_PATH_BASE}" "${CI_CACHE_NIT_HASHDIR}" -ls || true rm -rf "${TESTCERT_PATH_BASE}" "${CI_CACHE_NIT_HASHDIR}" || true mkdir -p "${TESTCERT_PATH_BASE}" "${CI_CACHE_NIT_HASHDIR}" fi @@ -1216,6 +1209,34 @@ if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] ; then fi fi +# NOTE: We only check for command-line tooling if we need to generate +# certs *now* (we can use cached/tarballed ones without that). +case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *NSS*) + (command -v certutil) || { + if [ x"${WITH_SSL_TESTS}" = xrequired-conditional ] ; then + die "Aborting because SSL tests are required, but needed third-party tooling was not found to produce the crypto credential stores for NSS" + fi + log_warn "NUT can use NSS, but needed third-party tooling was not found to produce the crypto credential stores" + if [ x"${WITH_SSL_CLIENT}" = xNSS ] ; then WITH_SSL_CLIENT="none" ; fi + if [ x"${WITH_SSL_SERVER}" = xNSS ] ; then WITH_SSL_SERVER="none" ; fi + } + ;; +esac + +case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in + *OpenSSL*) + (command -v openssl) || { + if [ x"${WITH_SSL_TESTS}" = xrequired-conditional ] ; then + die "Aborting because SSL tests are required, but needed third-party tooling was not found to produce the crypto credential stores for OpenSSL" + fi + log_warn "NUT can use OpenSSL, but needed third-party tooling was not found to produce the crypto credential stores" + if [ x"${WITH_SSL_CLIENT}" = xOpenSSL ] ; then WITH_SSL_CLIENT="none" ; fi + if [ x"${WITH_SSL_SERVER}" = xOpenSSL ] ; then WITH_SSL_SERVER="none" ; fi + } + ;; +esac + # Follow docs/security.txt points about setting up the crypto material # stores and their contents (mock a self-signed CA here where appropriate) # For a good summary of OpenSSL options and decent example config see e.g. @@ -1268,7 +1289,9 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in if [ -d "${CI_CACHE_NIT_HASHDIR}" ] ; then log_info "Found cached NIT certificates in ${CI_CACHE_NIT_HASHDIR} after waiting" mkdir -p "${TESTCERT_PATH_BASE}" - cp -pr "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}/" + # See comments above about no TESTCERT_PATH_SEP for shell globs. + cp -prf "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" || \ + cp -prfL "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" if check_NIT_certs ; then rm -f "${LOCKFILE}" @@ -1469,10 +1492,22 @@ EOF && [ -n "${CERTHASH}" ] \ || die "Could not determine OpenSSL certificate hash for Root CA files" - # NOTE: Symlinking may be prohibited or not implemented on some platforms (e.g. Windows) or file systems - ln -fs rootca.pem "${CERTHASH}".0 || ln -f rootca.pem "${CERTHASH}".0 || cp -f rootca.pem "${CERTHASH}".0 - ln -fs rootca.pem "${CERTHASH}" || ln -f rootca.pem "${CERTHASH}" || cp -f rootca.pem "${CERTHASH}" - ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem "${TESTCERT_PATH_ROOTCA}/${CERTHASH}"* \ + # NOTE: Symlinking may be prohibited or not implemented + # on some platforms (e.g. Windows) or file systems, and + # even if we can create a symlink, we may have trouble + # copying it as such. + log_info "SSL: Preparing OpenSSL CA PEM file hash-named (${CERTHASH}) copies or links" + if [ x"${MSYSTEM}${MSYS2_PATH}${MSYSTEM_PREFIX}" = x ]; then + ln -fs rootca.pem "${CERTHASH}".0 || ln -f rootca.pem "${CERTHASH}".0 || cp -f rootca.pem "${CERTHASH}".0 + ln -fs rootca.pem "${CERTHASH}" || ln -f rootca.pem "${CERTHASH}" || cp -f rootca.pem "${CERTHASH}" + else + # On Windows/MSYS2, less hassle to just make copies: + cp -f rootca.pem "${CERTHASH}".0 + cp -f rootca.pem "${CERTHASH}" + fi + + # See comments above about no TESTCERT_PATH_SEP for shell globs. + ls -l "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem "${TESTCERT_PATH_ROOTCA}"/"${CERTHASH}"* \ || die "Could not list OpenSSL CA PEM file and hash links" } @@ -1481,7 +1516,7 @@ EOF openssl_hash_CAdir ;; *) - ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + ls -l "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ || die "Could not list OpenSSL CA PEM file (exported from NSS)" if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] \ @@ -1509,7 +1544,7 @@ EOF certutil -A -d . -f .pwfile \ -n "${TESTCERT_ROOTCA_NAME}" \ -t "CT,C,C" \ - -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -a -i "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ || die "Could not import the CA certificate to NSS Server database" # Create a server certificate request: @@ -1517,7 +1552,7 @@ EOF certutil -R -d . -f .pwfile \ -s "CN=${TESTCERT_SERVER_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" \ -a -o server.req \ - -z "${TESTCERT_PATH_ROOTCA}"/.random \ + -z "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}".random \ --extKeyUsage "serverAuth,critical" \ --nsCertType sslServer,critical \ --keyUsage critical,dataEncipherment,keyEncipherment,digitalSignature,nonRepudiation \ @@ -1530,7 +1565,7 @@ EOF # but generally we do not know how many questions are asked: cscmd() { certutil -C -d "${TESTCERT_PATH_ROOTCA}" \ - -f "${TESTCERT_PATH_ROOTCA}"/.pwfile \ + -f "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}".pwfile \ -c "${TESTCERT_ROOTCA_NAME}" \ -a -i server.req -o server.crt \ --extKeyUsage "serverAuth,critical" \ @@ -1628,9 +1663,10 @@ EOF mkpk12key -legacy && mkjks } fi + # See comments above about no TESTCERT_PATH_SEP for shell globs. ls -l "${TESTCERT_PATH_SERVER}"/*.jks "${TESTCERT_PATH_SERVER}"/*.p12 || true - cat server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem server.key > upsd.pem 2>/dev/null || true + cat server.crt "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem server.key > upsd.pem 2>/dev/null || true fi check_NIT_certs_NSS "Server" "${TESTCERT_PATH_SERVER}" @@ -1662,19 +1698,19 @@ EOF # Sign a certificate request with the CA certificate: ( cd "${TESTCERT_PATH_ROOTCA}" openssl x509 -req \ - -in "${TESTCERT_PATH_SERVER}/server.req" \ + -in "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}server.req" \ -passin file:.pwfile \ -CA rootca.pem -CAkey rootca.key \ -CAcreateserial \ - -out "${TESTCERT_PATH_SERVER}/server.crt" \ + -out "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}server.crt" \ -days "${TESTCERT_VALIDITY_DAYS}" -sha256 \ - -extfile "${TESTCERT_PATH_SERVER}/server.v3.ext" + -extfile "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}server.v3.ext" ) || die "Could not sign a OpenSSL Server certificate request with the OpenSSL CA certificate" - cat server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem server.key > upsd.pem \ + cat server.crt "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem server.key > upsd.pem \ || die "Could not combine an upsd.pem" - ls -l "${TESTCERT_PATH_SERVER}"/upsd.pem \ + ls -l "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"upsd.pem \ || die "Could not list an upsd.pem" if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] \ @@ -1691,13 +1727,13 @@ EOF certutil -A -d . -f .pwfile \ -n "${TESTCERT_ROOTCA_NAME}" \ -t "CT,C,C" \ - -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -a -i "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ || die "Could not import the CA certificate to NSS Server database" # Import Server certificate and key openssl pkcs12 -export -out server.p12 \ -inkey server.key -in server.crt \ - -certfile "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -certfile "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ -name "${TESTCERT_SERVER_NAME}" \ -passout file:.pwfile \ || die "Could not package Server cert to PKCS#12 for NSS import" @@ -1712,7 +1748,7 @@ EOF # Bonus program: Java JKS (if caching) openssl pkcs12 -export -out server.p12 \ -inkey server.key -in server.crt \ - -certfile "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -certfile "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ -name "${TESTCERT_SERVER_NAME}" \ -passout file:.pwfile \ && keytool -importkeystore \ @@ -1725,6 +1761,7 @@ EOF -alias "${TESTCERT_SERVER_NAME}" \ -noprompt \ && log_info "Generated Java JKS for Server (from OpenSSL)" + # See comments above about no TESTCERT_PATH_SEP for shell globs. ls -l "${TESTCERT_PATH_SERVER}"/*.jks "${TESTCERT_PATH_SERVER}"/*.p12 || true fi fi @@ -1746,7 +1783,7 @@ EOF certutil -A -d . -f .pwfile \ -n "${TESTCERT_ROOTCA_NAME}" \ -t "TC,," \ - -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -a -i "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ || die "Could not import the CA certificate to NSS Client database" # Import server cert into client database so we can trust it (CERTHOST directive): @@ -1758,7 +1795,7 @@ EOF # as an existing cert, but that is not the same cert. certutil -A -d . -f .pwfile \ -n "${TESTCERT_SERVER_NAME}" \ - -a -i "${TESTCERT_PATH_SERVER}/server.crt" \ + -a -i "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}server.crt" \ -t ",," \ || die "Could not import the Server certificate to NSS Client database" @@ -1767,7 +1804,7 @@ EOF certutil -R -d . -f .pwfile \ -s "CN=${TESTCERT_CLIENT_NAME},OU=Test,O=NIT,ST=StateOfChaos,C=US" \ -a -o client.req \ - -z "${TESTCERT_PATH_ROOTCA}"/.random \ + -z "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}".random \ || die "Could not create a NSS Client certificate request" # Sign a certificate request with the CA certificate: @@ -1776,7 +1813,7 @@ EOF # but generally we do not know how many questions are asked: cscmd() { certutil -C -d "${TESTCERT_PATH_ROOTCA}" \ - -f "${TESTCERT_PATH_ROOTCA}"/.pwfile \ + -f "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}".pwfile \ -c "${TESTCERT_ROOTCA_NAME}" \ -a -i client.req -o client.crt \ --extKeyUsage "clientAuth" \ @@ -1856,25 +1893,25 @@ IP.3 = 127.1.2.`expr $$ % 200` EOF # Sign a certificate request with the CA certificate: ( cd "${TESTCERT_PATH_ROOTCA}" - openssl x509 -req -in "${TESTCERT_PATH_CLIENT}/client.req" \ + openssl x509 -req -in "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}client.req" \ -passin file:.pwfile \ -CA rootca.pem -CAkey rootca.key -CAcreateserial \ - -out "${TESTCERT_PATH_CLIENT}/client.crt" \ + -out "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}client.crt" \ -days "${TESTCERT_VALIDITY_DAYS}" -sha256 \ - -extfile "${TESTCERT_PATH_CLIENT}/client.v3.ext" + -extfile "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}client.v3.ext" ) || die "Could not sign a OpenSSL Client certificate request with the OpenSSL CA certificate" - cat client.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem client.key > upsmon.pem \ + cat client.crt "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem client.key > upsmon.pem \ || die "Could not combine an upsmon.pem" - ls -l "${TESTCERT_PATH_CLIENT}"/upsmon.pem \ + ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"upsmon.pem \ || die "Could not list an upsmon.pem" log_info "SSL: Exporting public data of server certificate for client use..." - cat "${TESTCERT_PATH_SERVER}"/server.crt "${TESTCERT_PATH_ROOTCA}"/rootca.pem > upsd-public.pem \ + cat "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"server.crt "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem > upsd-public.pem \ || die "Could not combine a upsd-public.pem" - ls -l "${TESTCERT_PATH_CLIENT}/upsd-public.pem" \ + ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}upsd-public.pem" \ || die "Could not list a upsd-public.pem" if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] \ @@ -1883,13 +1920,13 @@ EOF ; then # Bonus program: Java JKS (if caching) keytool -importcert \ - -file "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -file "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ -alias "${TESTCERT_ROOTCA_NAME}" \ -keystore rootca.jks \ -storepass "${TESTCERT_ROOTCA_PASS}" \ -noprompt \ && keytool -importcert \ - -file "${TESTCERT_PATH_SERVER}"/server.crt \ + -file "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"server.crt \ -alias "${TESTCERT_SERVER_NAME}" \ -keystore rootca.jks \ -storepass "${TESTCERT_ROOTCA_PASS}" \ @@ -1912,13 +1949,13 @@ EOF certutil -A -d . -f .pwfile \ -n "${TESTCERT_ROOTCA_NAME}" \ -t "TC,," \ - -a -i "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -a -i "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ || die "Could not import the CA certificate to NSS Client database" # Import server cert into client database so we can trust it (CERTHOST directive): certutil -A -d . -f .pwfile \ -n "${TESTCERT_SERVER_NAME}" \ - -a -i "${TESTCERT_PATH_SERVER}/server.crt" \ + -a -i "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}server.crt" \ -t ",," \ || die "Could not import the Server certificate to NSS Client database" @@ -1926,7 +1963,7 @@ EOF # Import Client certificate and key openssl pkcs12 -export -out client.p12 \ -inkey client.key -in client.crt \ - -certfile "${TESTCERT_PATH_ROOTCA}"/rootca.pem \ + -certfile "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem \ -name "${TESTCERT_CLIENT_NAME}" \ -passout file:.pwfile \ || die "Could not package Client cert to PKCS#12 for NSS import" @@ -1971,9 +2008,9 @@ EOF if [ ! -d "${CI_CACHE_NIT_HASHDIR}" ] ; then log_info "Populating NIT certificate cache in ${CI_CACHE_NIT_HASHDIR}" mkdir -p "${CI_CACHE_NIT_HASHDIR}" - cp -pr "${TESTCERT_PATH_BASE}"/* "${CI_CACHE_NIT_HASHDIR}/" + cp -prf "${TESTCERT_PATH_BASE}"/* "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}" set | ${EGREP} '^TESTCERT[^ ]*=' | grep -v PATH \ - > "${CI_CACHE_NIT_HASHDIR}/TESTCERT_VARS.env" + > "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}TESTCERT_VARS.env" fi rm -f "${CI_CACHE_NIT_HASHDIR}.lock" fi @@ -1997,7 +2034,7 @@ esac # SSL_CERT_DIR="${TESTCERT_PATH_ROOTCA}" # export SSL_CERT_DIR # -# SSL_CERT_FILE="${TESTCERT_PATH_ROOTCA}/rootca.pem" +# SSL_CERT_FILE="${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}rootca.pem" # export SSL_CERT_FILE #fi } @@ -2025,7 +2062,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 @@ -2165,6 +2202,7 @@ EOF ### upsd.users: ################################################## TESTPASS_ADMIN='mypass' +TESTPASS_READER='public' TESTPASS_TESTER='pass words' TESTPASS_UPSMON_PRIMARY='P@ssW0rdAdm' TESTPASS_UPSMON_SECONDARY='P@ssW0rd' @@ -2176,6 +2214,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 @@ -2391,19 +2433,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 @@ -2415,6 +2457,164 @@ 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: + [ x"${WITH_SSL_CLIENT}" = xnone ] || \ + 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() { @@ -2698,6 +2898,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 @@ -2804,6 +3005,7 @@ generatecfg_sandbox() { generatecfg_upsd_nodev generatecfg_upsd_add_SSL generatecfg_upsdusers_trivial + generatecfg_nutauth generatecfg_ups_dummy } @@ -3384,8 +3586,8 @@ setenv_ssl_common() { NUT_CAPATH="${TESTCERT_PATH_ROOTCA}" NUT_CERTVERIFY=1 export NUT_CAPATH NUT_CERTVERIFY - else if test -s "${TESTCERT_PATH_ROOTCA}"/rootca.pem ; then - NUT_CAFILE="${TESTCERT_PATH_ROOTCA}"/rootca.pem + else if test -s "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem ; then + NUT_CAFILE="${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem NUT_CERTVERIFY=1 export NUT_CAFILE NUT_CERTVERIFY fi ; fi @@ -3446,8 +3648,8 @@ setenv_ssl_cppnit() { NUT_CAPATH="${TESTCERT_PATH_ROOTCA}" NUT_CERTVERIFY=1 export NUT_CAPATH NUT_CERTVERIFY - else if test -s "${TESTCERT_PATH_ROOTCA}"/rootca.pem ; then - NUT_CAFILE="${TESTCERT_PATH_ROOTCA}"/rootca.pem + else if test -s "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem ; then + NUT_CAFILE="${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem NUT_CERTVERIFY=1 export NUT_CAFILE NUT_CERTVERIFY fi ; fi @@ -3658,7 +3860,7 @@ isTestablePerl() { return 1 fi - if [ ! -s "${TOP_SRCDIR}/scripts/perl/UPS/Nut.pm" ] \ + if [ ! -s "${TOP_BUILDDIR}/scripts/perl/UPS/Nut.pm" ] \ ; then return 1 fi @@ -3688,7 +3890,7 @@ isTestablePerl() { log_error "[isTestablePerl] Detected perl shebang: '${PL_SHEBANG}' (result=${PL_RES})" fi - PERL_OPTS_INC="-I${TOP_SRCDIR}/scripts/perl" + PERL_OPTS_INC="-I${TOP_BUILDDIR}/scripts/perl" PERL_OPTS_DEBUG='' if [ x"$NIT_DEBUG_PERL" = xtrue ] ; then if [ -d "${HOME}/perl5/lib/perl5" ] ; then diff --git a/tests/test_authconf.c b/tests/test_authconf.c new file mode 100644 index 0000000000..2ff2df8bf7 --- /dev/null +++ b/tests/test_authconf.c @@ -0,0 +1,572 @@ +/* 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, expected_sections = 0; + 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; + } + + expected_sections++; + fprintf(f, "USER = globaluser\n"); + fprintf(f, "PASS = globalpass\n"); + fprintf(f, "CERTVERIFY = 1\n"); + fprintf(f, "INCLUDE %s\n", include_conf); + + expected_sections++; + fprintf(f, "[@localhost:12345]\n"); + fprintf(f, " USER = hostuser\n"); + fprintf(f, " FORCESSL = 1\n"); + + expected_sections++; + 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; + } + + /* NOTE: Non-commented tokens are probably also ignored */ + expected_sections++; + fprintf(f, "[@otherhost] # Other (commented) tokens ignored\n"); + fprintf(f, " USER = otheruser\n"); + fprintf(f, " CERTHOST = \"Other Server\"\n"); + + /* A link-local (MAC address based) IPv6 address as colon-separated hexes in square brackets + * Here and below - essentially a test for upscli_split_authconf_section() method. + * A missing '@' should mean the section name is wholly the host name (maybe with port) */ + expected_sections++; + fprintf(f, "[[fe80::215:5dff:fea4:f780]]\n"); + fprintf(f, " CERTHOST = \"An IPv6 Server\"\n"); + + expected_sections++; + fprintf(f, "[ipv6user@[fe80::215:5dff:fea4:f780]]\n"); + fprintf(f, " PASS = \"ipv6pass\"\n"); + + expected_sections++; + fprintf(f, "[[fe80::215:5dff:fea4:f781]:3495]\n"); + fprintf(f, " CERTHOST = \"An IPv6 Server on port 3495\"\n"); + + /* FIXME: With proper IPv6 parsing, these two should collapse into the same section + * or error out in case of conflicts/redefinitions */ + expected_sections++; + fprintf(f, "[@[0::1]:12345]\n"); + fprintf(f, " USER = IPv6user\n"); + fprintf(f, " CERTHOST = \"An IPv6 localhost Server\"\n"); + expected_sections++; + fprintf(f, "[@[::1]:12345]\n"); + fprintf(f, " USER = IPv6user2\n"); + fprintf(f, " CERTHOST = \"An IPv6 localhost 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 %" PRIuSIZE " sections (including global)\n", num_sections == expected_sections ? "" : "not ", ++testnum, expected_sections); + + /* 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 %" PRIuSIZE " sections (including global)\n", num_sections == expected_sections ? "" : "not ", ++testnum, expected_sections); + + /* 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); + expected_sections++; + 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 (already is there) */ + 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); + expected_sections++; + 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. IPv6#1 in brackets */ + printf("Checking IPv6#1 match for a link-local address section\n"); + ac = upscli_find_authconf_item(NULL, "[fe80::215:5dff:fea4:f780]", NULL); + if (ac) { + /* Normalized, with default port injected */ + if (ac->section && !strcmp(ac->section, "@[fe80::215:5dff:fea4:f780]:3493")) { + printf("ok %d - got expected bracketed IPv6#1 address as the section name\n", ++testnum); + } else { + printf("not ok %d - did not get expected bracketed IPv6#1 address as the section name\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("not ok %d - did not get any section by expected bracketed IPv6#1 address\n", ++testnum); + return 1; + } + + /* 17. Data in that IPv6#1 section... */ + if (ac->certhost && !strcmp(ac->certhost, "An IPv6 Server")) { + printf("ok %d - got expected CERTHOST in IPv6#1 section\n", ++testnum); + } else { + printf("not ok %d - did not get expected CERTHOST in IPv6#1 section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + /* FIXME: Find a host_cert for the bracketed IPv6 address, make sure it is the same as the one in the section */ + + /* 18. user@IPv6#1 in brackets */ + printf("Checking user@IPv6#1 match for a link-local address section\n"); + ac = upscli_find_authconf_item("ipv6user", "[fe80::215:5dff:fea4:f780]", NULL); + if (ac) { + if (ac->section && !strcmp(ac->section, "ipv6user@[fe80::215:5dff:fea4:f780]:3493")) { + printf("ok %d - got expected bracketed user@IPv6#1 address as the section name\n", ++testnum); + } else { + printf("not ok %d - did not get expected bracketed user@IPv6#1 address as the section name\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("not ok %d - did not get any section by expected bracketed user@IPv6#1 address\n", ++testnum); + return 1; + } + + /* 19. Data in that user@IPv6#1 section, unique... */ + if (ac->pass && !strcmp(ac->pass, "ipv6pass")) { + printf("ok %d - got expected PASS in user@IPv6#1 section\n", ++testnum); + } else { + printf("not ok %d - did not get expected PASS in user@IPv6#1 section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + + /* 20. Data in that user@IPv6#1 section, inherited - should be none for find method... */ + if (!(ac->certhost)) { + printf("ok %d - no expected CERTHOST in user@IPv6#1 section\n", ++testnum); + } else { + printf("not ok %d - got an unexpected CERTHOST in user@IPv6#1 section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + + /* 21. Re-probe with get method */ + printf("Checking user@IPv6#1 CERTHOST match for a link-local address section after upscli_get_authconf_item(), already in list - updated in place\n"); + ac = upscli_get_authconf_item("ipv6user", "[fe80::215:5dff:fea4:f780]", NULL, 1); + /* NOTE: Not bumping expected_sections because the section is already in the list */ + if (ac) { + if (ac->section && !strcmp(ac->section, "ipv6user@[fe80::215:5dff:fea4:f780]:3493")) { + printf("ok %d - got expected bracketed user@IPv6#1 address as the section name\n", ++testnum); + } else { + printf("not ok %d - did not get expected bracketed user@IPv6#1 address as the section name\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("not ok %d - did not get any section by expected bracketed user@IPv6#1 address\n", ++testnum); + return 1; + } + + /* 22. Data in that user@IPv6#1 section, now inherited... */ + if (ac->certhost && !strcmp(ac->certhost, "An IPv6 Server")) { + printf("ok %d - got expected CERTHOST in user@IPv6#1 section\n", ++testnum); + } else { + printf("not ok %d - did not get expected CERTHOST in user@IPv6#1 section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + + /* 23. IPv6#2 in brackets */ + printf("Checking NO IPv6#2 match for a link-local address section without a port\n"); + ac = upscli_find_authconf_item(NULL, "[fe80::215:5dff:fea4:f781]", NULL); + if (ac) { + if (ac->section) { + printf("not ok %d - got a hit by expected bracketed IPv6#2 address as the section name but without asking for the custom port\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } else { + printf("ok %d - got the global section by expected bracketed IPv6#2 address when not asking for the custom port\n", ++testnum); + } + } else { + printf("ok %d - (sort of OK - expected global section) did not get any section by expected bracketed IPv6#2 address when not asking for the custom port\n", ++testnum); + } + + /* 24. IPv6#2 in brackets */ + printf("Checking IPv6#2 match for a link-local address section\n"); + ac = upscli_find_authconf_item(NULL, "[fe80::215:5dff:fea4:f781]", "3495"); + if (ac) { + if (ac->section && !strcmp(ac->section, "@[fe80::215:5dff:fea4:f781]:3495")) { + printf("ok %d - got expected bracketed IPv6#2 address and port as the section name\n", ++testnum); + } else { + printf("not ok %d - did not get expected bracketed IPv6#2 address and port as the section name\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("not ok %d - did not get any section by expected bracketed IPv6#2 address and port\n", ++testnum); + return 1; + } + + /* 25. Data in that IPv6#2 section... */ + if (ac->certhost && !strcmp(ac->certhost, "An IPv6 Server on port 3495")) { + printf("ok %d - got expected CERTHOST in IPv6#2 section\n", ++testnum); + } else { + printf("not ok %d - did not get expected CERTHOST in IPv6#2 section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + /* FIXME: Find a host_port_cert for the bracketed IPv6 address, make sure it is the same + * as the one in the section, and there are no hits for any other ports */ + + /* 26. IPv6#3 in brackets */ + printf("Checking NO IPv6#3 match for a localhost address section without a port\n"); + ac = upscli_find_authconf_item(NULL, "[0::1]", NULL); + if (ac) { + if (ac->section) { + printf("not ok %d - got a hit by expected bracketed IPv6#3 address as the section name but without asking for the custom port\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } else { + printf("ok %d - (sort of) got the global section by expected bracketed IPv6#3 address when not asking for the custom port\n", ++testnum); + } + } else { + printf("ok %d - did not get any section by expected bracketed IPv6#3 address when not asking for the custom port\n", ++testnum); + } + + /* 27. IPv6#3a in brackets */ + printf("Checking IPv6#3a match for a localhost address section\n"); + ac = upscli_find_authconf_item(NULL, "[::1]", "12345"); + if (ac) { + if (ac->section && !strcmp(ac->section, "@[::1]:12345")) { + printf("ok %d - got expected bracketed IPv6#3a address and port as the section name\n", ++testnum); + } else { + printf("not ok %d - did not get expected bracketed IPv6#3a address and port as the section name\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("not ok %d - did not get any section by expected bracketed IPv6#3a address and port\n", ++testnum); + return 1; + } + + /* 28. Data in that IPv6#3 section... */ + /* FIXME: With proper IPv6 parsing, [::1] and [0::1] should collapse into the same section + * or error out in case of conflicts/redefinitions; for now they are treated as separate */ + if (ac->user && !strcmp(ac->user, "IPv6user2")) { + printf("ok %d - got expected USERNAME in IPv6#3a section\n", ++testnum); + } else { + printf("not ok %d - did not get expected USERNAME in IPv6#3a section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + + /* 29. IPv6#3b in brackets */ + printf("Checking IPv6#3b match for a localhost address section\n"); + ac = upscli_find_authconf_item(NULL, "[0::1]", "12345"); + if (ac) { + if (ac->section && !strcmp(ac->section, "@[0::1]:12345")) { + printf("ok %d - got expected bracketed IPv6#3b address and port as the section name\n", ++testnum); + } else { + printf("not ok %d - did not get expected bracketed IPv6#3b address and port as the section name\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + } else { + printf("not ok %d - did not get any section by expected bracketed IPv6#3a address and port\n", ++testnum); + return 1; + } + + /* 30. Data in that IPv6#3 section... */ + /* FIXME: see above */ + if (ac->user && !strcmp(ac->user, "IPv6user")) { + printf("ok %d - got expected USERNAME in IPv6#3b section\n", ++testnum); + } else { + printf("not ok %d - did not get expected USERNAME in IPv6#3b section\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1, 1); + return 1; + } + + /* 31. 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 %" PRIuSIZE " sections (including global and cached results), expected %" PRIuSIZE "\n", + num_sections == expected_sections ? "" : "not ", ++testnum, num_sections, expected_sections); + + 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; +} diff --git a/tools/nut-scanner/Makefile.am b/tools/nut-scanner/Makefile.am index 9bf2c9d4bb..c52dbbd3e9 100644 --- a/tools/nut-scanner/Makefile.am +++ b/tools/nut-scanner/Makefile.am @@ -142,7 +142,7 @@ libnutscan_la_LDFLAGS += @NETLIBS_GETADDRS@ # libnutscan version information ### WARNING: Do not forget to update SO_MAJOR_LIBNUTSCAN under scripts/obs, ### especially when bumping "age" into loss of compatibility with old releases! -libnutscan_la_LDFLAGS += -version-info 5:0:1 +libnutscan_la_LDFLAGS += -version-info 6:0:2 # libnutscan exported symbols regex # WARNING: Since the library includes parts of libcommon (as much as needed diff --git a/tools/nut-scanner/nut-scan.h b/tools/nut-scanner/nut-scan.h index 1619325e99..c37fc049e5 100644 --- a/tools/nut-scanner/nut-scan.h +++ b/tools/nut-scanner/nut-scan.h @@ -202,6 +202,17 @@ typedef struct nutscan_usb { int report_bcdDevice; } nutscan_usb_t; +typedef struct nutscan_nut_authconf { + const char * authconf_file; /* where to load authconf data from; "none" means to ignore auth config even if it exists */ + useconds_t usec_timeout; /* Wait this long for a response */ + const char * port_string; /* We can pass a port name like "nut" and resolve it inside */ + + /* Added for consistency with other structs; not used at the moment; + * practically see also `struct nut_scan_arg` in `scan_nut.c`: */ + const char * peername; + uint16_t port_number; +} nutscan_nut_authconf_t; + /* Scanning */ nutscan_device_t * nutscan_scan_snmp(const char * start_ip, const char * stop_ip, useconds_t usec_timeout, nutscan_snmp_t * sec); nutscan_device_t * nutscan_scan_ip_range_snmp(nutscan_ip_range_list_t * irl, useconds_t usec_timeout, nutscan_snmp_t * sec); @@ -215,6 +226,8 @@ nutscan_device_t * nutscan_scan_ip_range_xml_http(nutscan_ip_range_list_t * irl, nutscan_device_t * nutscan_scan_nut(const char * startIP, const char * stopIP, const char * port, useconds_t usec_timeout); nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, const char * port, useconds_t usec_timeout); +nutscan_device_t * nutscan_scan_nut_authconf(const char * startIP, const char * stopIP, nutscan_nut_authconf_t *sec); +nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * irl, nutscan_nut_authconf_t *sec); nutscan_device_t * nutscan_scan_nut_simulation(void); diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index c22bdb7d9b..4700b5387a 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -38,6 +38,11 @@ int nutscan_unload_upsclient_library(void); #define SCAN_NUT_DRIVERNAME "dummy-ups" +/* Same default timeout as in upsc and other clients, but numeric. + * Handled via nut_upscli_init_default_connect_timeout() if detected, + * or as the fallback default if not (and none passed by C caller). */ +#define UPSCLI_DEFAULT_CONNECT_TIMEOUT_SEC 10 + /* dynamic link library stuff */ static lt_dlhandle dl_handle = NULL; static const char *dl_error = NULL; @@ -57,6 +62,20 @@ static void (*nut_upscli_upslog_setproctag)(const char *tag, const void *cookie) static void (*nut_upscli_upslog_setprocname)(const char *tag, const void *cookie); static struct timeval *(*nut_upscli_upslog_start_sync)(struct timeval *tv, const void *cookie); static void (*nut_upscli_report_build_details)(void); +static int (*nut_upscli_init_default_connect_timeout)(const char *cli_secs, + const char *config_secs, const char *default_secs); +static upscli_authconf_t *(*nut_upscli_get_authconf_item)(const char *user, + const char *host, const char *port, int add_to_list); +static int (*nut_upscli_init_authconf)(upscli_authconf_t *ac); +static upscli_authconf_t *(*nut_upscli_find_authconf_item)(const char *user, + const char *host, const char *port); +static void (*nut_upscli_free_authconf_item)(upscli_authconf_t *ac); +static int (*nut_upscli_read_authconf_file)(const char *filename, int fatal_errors); +static int (*nut_upscli_authenticate_authconf)(UPSCONN_t *ups, upscli_authconf_t *ac); +static void (*nut_upscli_get_default_connect_timeout)(struct timeval *ptv); +static void (*nut_upscli_free_host_cert)(const char *hostname, const char *certname); +static void (*nut_upscli_free_host_port_cert)(const char *hostname, uint16_t port, const char *certname); +static void (*nut_upscli_authconf_update_conn_flags)(const upscli_authconf_t *ac, int *flags); /* This variable collects device(s) from a sequential or parallel scan, * is returned to caller, and cleared to allow subsequent independent scans */ @@ -77,6 +96,8 @@ struct scan_nut_arg { * address, and/or :port suffix (if customized so): */ char * hostname; useconds_t timeout; + int flags_ssl; + upscli_authconf_t *ac_current; }; /* Return 0 on success, -1 on error e.g. "was not loaded"; @@ -256,6 +277,94 @@ int nutscan_load_upsclient_library(const char *libname_path) (*nut_upscli_report_build_details)(); } + *(void **) (&nut_upscli_init_default_connect_timeout) = lt_dlsym(dl_handle, + symbol = "upscli_init_default_connect_timeout"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_init_default_connect_timeout = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_get_authconf_item) = lt_dlsym(dl_handle, + symbol = "upscli_get_authconf_item"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_get_authconf_item = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_init_authconf) = lt_dlsym(dl_handle, + symbol = "upscli_init_authconf"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_init_authconf = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_find_authconf_item) = lt_dlsym(dl_handle, + symbol = "upscli_find_authconf_item"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_find_authconf_item = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_free_authconf_item) = lt_dlsym(dl_handle, + symbol = "upscli_free_authconf_item"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_free_authconf_item = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_read_authconf_file) = lt_dlsym(dl_handle, + symbol = "upscli_read_authconf_file"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_read_authconf_file = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_authenticate_authconf) = lt_dlsym(dl_handle, + symbol = "upscli_authenticate_authconf"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_authenticate_authconf = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_get_default_connect_timeout) = lt_dlsym(dl_handle, + symbol = "upscli_get_default_connect_timeout"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_get_default_connect_timeout = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_free_host_cert) = lt_dlsym(dl_handle, + symbol = "upscli_free_host_cert"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_free_host_cert = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_free_host_port_cert) = lt_dlsym(dl_handle, + symbol = "upscli_free_host_port_cert"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_free_host_port_cert = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + + *(void **) (&nut_upscli_authconf_update_conn_flags) = lt_dlsym(dl_handle, + symbol = "upscli_authconf_update_conn_flags"); + if ((dl_error = lt_dlerror()) != NULL) { + nut_upscli_authconf_update_conn_flags = NULL; + upsdebugx(1, "%s: %s() not found, using older libupsclient build?", + __func__, symbol); + } + /* Passed final lt_dlsym() */ symbol = NULL; @@ -322,7 +431,7 @@ static void * list_nut_devices_thready(void * arg) goto end; } - if ((*nut_upscli_tryconnect)(ups, hostname, port, UPSCLI_CONN_TRYSSL, &tv) < 0) { + if ((*nut_upscli_tryconnect)(ups, hostname, port, nut_arg->flags_ssl, &tv) < 0) { /* Avoid disconnect from not connected ups */ upsdebugx(4, "%s: upscli_tryconnect() failed", __func__); if (ups) { @@ -334,6 +443,13 @@ static void * list_nut_devices_thready(void * arg) goto end; } + /* Best-effort login (if present in the file for that host, or default) */ + if (nut_upscli_authenticate_authconf != NULL && nut_arg->ac_current != NULL + && nut_arg->ac_current->user && nut_arg->ac_current->pass + ) { + (*nut_upscli_authenticate_authconf)(ups, nut_arg->ac_current); + } + if ((*nut_upscli_list_start)(ups, numq, query) < 0) { upsdebugx(4, "%s: upscli_list_start() failed", __func__); goto end; @@ -425,6 +541,22 @@ static void * list_nut_devices_thready(void * arg) } nutscan_device_t * nutscan_scan_nut(const char* start_ip, const char* stop_ip, const char* port, useconds_t usec_timeout) +{ + nutscan_nut_authconf_t sec; + sec.usec_timeout = usec_timeout; + sec.port_string = port; + + /* Best-effort use of a user- or system- provided file, okay if absent */ + sec.authconf_file = NULL; + + /* UNUSED so far: */ + sec.peername = NULL; + sec.port_number = 0; /* we pass the port strings in args, to be resolved later */ + + return nutscan_scan_nut_authconf(start_ip, stop_ip, &sec); +} + +nutscan_device_t * nutscan_scan_nut_authconf(const char* start_ip, const char* stop_ip, nutscan_nut_authconf_t *sec) { nutscan_device_t *ndret; nutscan_ip_range_list_t irl; @@ -432,7 +564,7 @@ nutscan_device_t * nutscan_scan_nut(const char* start_ip, const char* stop_ip, c nutscan_init_ip_ranges(&irl); nutscan_add_ip_range(&irl, (char *)start_ip, (char *)stop_ip); - ndret = nutscan_scan_ip_range_nut(&irl, port, usec_timeout); + ndret = nutscan_scan_ip_range_nut_authconf(&irl, sec); /* Avoid nuking caller's strings here */ irl.ip_ranges->start_ip = NULL; @@ -443,6 +575,22 @@ nutscan_device_t * nutscan_scan_nut(const char* start_ip, const char* stop_ip, c } nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, const char* port, useconds_t usec_timeout) +{ + nutscan_nut_authconf_t sec; + sec.usec_timeout = usec_timeout; + sec.port_string = port; + + /* Best-effort use of a user- or system- provided file, okay if absent */ + sec.authconf_file = NULL; + + /* UNUSED so far: */ + sec.peername = NULL; + sec.port_number = 0; /* we pass the port strings in args, to be resolved later */ + + return nutscan_scan_ip_range_nut_authconf(irl, &sec); +} + +nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * irl, nutscan_nut_authconf_t *sec) { bool_t pass = TRUE; /* Track that we may spawn a scanning thread */ nutscan_ip_range_list_iter_t ip; @@ -457,7 +605,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons #ifdef HAVE_PTHREAD # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) - sem_t * semaphore = nutscan_semaphore(); + sem_t * semaphore = NULL; # if (defined HAVE_SEMAPHORE_UNNAMED) sem_t semaphore_scantype_inst; sem_t * semaphore_scantype = &semaphore_scantype_inst; @@ -472,6 +620,90 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons size_t max_threads_scantype = max_threads_oldnut; # endif + const char *nutauth = sec ? sec->authconf_file : NULL; + upscli_authconf_t *ac_default = NULL; + int flags_ssl = UPSCLI_CONN_TRYSSL; + + int have_nutauth_methods = ( + nut_upscli_authenticate_authconf != NULL && + nut_upscli_read_authconf_file != NULL && + nut_upscli_init_authconf != NULL && + nut_upscli_find_authconf_item != NULL && + nut_upscli_get_authconf_item != NULL && + nut_upscli_free_authconf_item != NULL + ); + + /* Technically speaking, this variable should hold values + * up to 1000000, but in practice would be at least 31-bit. + * If no spec from caller, apply the default we have. */ + useconds_t usec_timeout = sec ? sec->usec_timeout : (useconds_t)UPSCLI_DEFAULT_CONNECT_TIMEOUT_SEC * (1000*1000); + + if (nut_upscli_init_default_connect_timeout + && nut_upscli_get_default_connect_timeout + ) { + /* If the method is present, let the library know what timeout we want */ + char buf2[SMALLBUF]; + + if (sec) + snprintf(buf, sizeof(buf), "%g", (double)sec->usec_timeout / 1000000); + snprintf(buf2, sizeof(buf2), "%" PRIuMAX, (uintmax_t)UPSCLI_DEFAULT_CONNECT_TIMEOUT_SEC); + + if ((*nut_upscli_init_default_connect_timeout)( + sec ? buf : NULL, + NULL, buf2) >= 0 + ) { + struct timeval tv; + (*nut_upscli_get_default_connect_timeout)(&tv); + usec_timeout = tv.tv_sec * (1000*1000) + tv.tv_usec; + } else { + upsdebugx(1, "%s: upscli_init_default_connect_timeout() failed: " + "invalid network timeout was requested, using initial default: %" + PRIuMAX "usec", __func__, (uintmax_t)usec_timeout); + } + } + + if (nutauth && *nutauth && strcmp(nutauth, "none")) { + /* Non-trivial, not a skip */ + if (!have_nutauth_methods) { + upslogx(LOG_ERR, "A NUT auth config file '%s' was required, but needed methods are missing in loaded libupsclient", nutauth); + return NULL; + } + } + + if (nutauth && *nutauth) { + /* If we are here, needed method pointers are non-NULL */ + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "%s: Using nutauth='%s': skipping NUT auth config", __func__, nutauth); + } else { + /* Not passing fatal_errors=1 into the parser due to JSON support */ + int parsed = -1; + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "%s: Using nutauth='%s': require a user or system provided NUT auth config file", __func__, nutauth); + parsed = (*nut_upscli_read_authconf_file)(NULL, 0); + } else { + upsdebugx(1, "%s: Using nutauth='%s': require this NUT auth config file", __func__, nutauth); + parsed = (*nut_upscli_read_authconf_file)(nutauth, 0); + } + if (parsed < 0) { + upslogx(LOG_ERR, "A NUT auth config file '%s' was required, but we failed to parse it", nutauth); + return NULL; + } + } + } else { + if (nut_upscli_read_authconf_file) { + upsdebugx(1, "%s: Using best-effort NUT auth config detection", __func__); + (*nut_upscli_read_authconf_file)(NULL, 0); + } else { + upsdebugx(1, "%s: NOT using best-effort NUT auth config detection: upscli_read_authconf_file() not available", __func__); + } + } + +#ifdef HAVE_PTHREAD +# if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) + semaphore = nutscan_semaphore(); +# endif +#endif + pthread_mutex_init(&dev_mutex, NULL); # if (defined HAVE_SEMAPHORE_UNNAMED) || (defined HAVE_SEMAPHORE_NAMED) @@ -558,6 +790,13 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons ip_str = nutscan_ip_ranges_iter_init(&ip, irl); + if (nut_upscli_find_authconf_item != NULL) { + ac_default = (*nut_upscli_find_authconf_item)(NULL, NULL, NULL); + if (ac_default && nut_upscli_authconf_update_conn_flags != NULL) { + (*nut_upscli_authconf_update_conn_flags)(ac_default, &flags_ssl); + } + } + while (ip_str != NULL) { #ifdef HAVE_PTHREAD /* NOTE: With many enough targets to scan, this can crash @@ -674,12 +913,12 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons #endif /* HAVE_PTHREAD */ if (pass) { - if (port) { + if (sec && sec->port_string && *(sec->port_string)) { if (ip.curr_ip_iter.type == IPv4) { - snprintf(buf, sizeof(buf), "%s:%s", ip_str, port); + snprintf(buf, sizeof(buf), "%s:%s", ip_str, sec->port_string); } else { - snprintf(buf, sizeof(buf), "[%s]:%s", ip_str, port); + snprintf(buf, sizeof(buf), "[%s]:%s", ip_str, sec->port_string); } ip_dest = strdup(buf); @@ -694,8 +933,28 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons break; } + /* NOTE: Above we defer to nut_upscli_init_default_connect_timeout() + * when present and may fall back to envvars like other NUT clients, + * if no value was passed by C API caller */ nut_arg->timeout = usec_timeout; nut_arg->hostname = ip_dest; + nut_arg->flags_ssl = flags_ssl; + + if (have_nutauth_methods) { + /* FIXME [#3494]: 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. */ + /* NOTE: Unlike other clients, here we DO NOT add the item to list, + * so we can forget it soon without hassle */ + nut_arg->ac_current = (*nut_upscli_get_authconf_item)(NULL, ip_str, sec ? sec->port_string : NULL, 0); + /* Always call upscli_init_authconf(), to register possible CERTHOSTs etc. */ + if ((*nut_upscli_init_authconf)(nut_arg->ac_current) > 0 && nut_arg->ac_current != NULL && nut_upscli_authconf_update_conn_flags != NULL) { + (*nut_upscli_authconf_update_conn_flags)(nut_arg->ac_current, &(nut_arg->flags_ssl)); + } + } else { + nut_arg->ac_current = NULL; + } #ifdef HAVE_PTHREAD if (pthread_create(&thread, NULL, list_nut_devices_thready, (void*)nut_arg) == 0) { @@ -726,12 +985,22 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons list_nut_devices_thready(nut_arg); #endif /* if HAVE_PTHREAD */ + /* NOTE: Work with host_cert list is + * mutex'ed in the upsclient library */ + if (nut_upscli_free_host_cert) { + (*nut_upscli_free_host_cert)(ip_dest, NULL); + } + /* Prepare the next iteration; note that * nutscan_scan_ipmi_device_thready() * takes care of freeing "tmp_sec" and its * copy (note strdup!) of "ip_str" as * hostname, possibly suffixed with a port. */ + if (nut_arg->ac_current && have_nutauth_methods) { + (*nut_upscli_free_authconf_item)(nut_arg->ac_current); + nut_arg->ac_current = NULL; + } free(ip_str); ip_str = nutscan_ip_ranges_iter_inc(&ip); } else { /* if not pass -- all slots busy */