From 09cf5f2c6fcafa7ec91958628bc6505f30fdc797 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 11 May 2026 20:04:22 +0200 Subject: [PATCH 001/108] Introduce NUT "authconf" file support [#3329] First PoC from AI, slightly modified in review, following the spec requested in the GitHub issue. Stepping stone for further work. Signed-off-by: Jim Klimov --- NEWS.adoc | 2 + clients/Makefile.am | 4 +- clients/authconf.c | 457 +++++++++++++++++++++++++ clients/authconf.h | 69 ++++ docs/man/Makefile.am | 36 ++ docs/man/nutauth.conf.txt | 152 ++++++++ docs/man/upscli_dump_authconf_list.txt | 44 +++ docs/man/upscli_find_authconf.txt | 57 +++ docs/man/upscli_get_authconf_list.txt | 61 ++++ docs/man/upscli_read_authconf.txt | 58 ++++ docs/nut.dict | 4 + tests/Makefile.am | 8 + tests/test_authconf.c | 220 ++++++++++++ 13 files changed, 1170 insertions(+), 2 deletions(-) create mode 100644 clients/authconf.c create mode 100644 clients/authconf.h create mode 100644 docs/man/nutauth.conf.txt create mode 100644 docs/man/upscli_dump_authconf_list.txt create mode 100644 docs/man/upscli_find_authconf.txt create mode 100644 docs/man/upscli_get_authconf_list.txt create mode 100644 docs/man/upscli_read_authconf.txt create mode 100644 tests/test_authconf.c diff --git a/NEWS.adoc b/NEWS.adoc index dbc95c6dbb..27d6e3dd8e 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -126,6 +126,8 @@ 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] - `upsmon` client updates: * Introduced support for `CERTFILE` option, so the client can identify 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..5668789111 --- /dev/null +++ b/clients/authconf.c @@ -0,0 +1,457 @@ +/* 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 +#include +#include + +static upscli_authconf_t *authconf_list = NULL; +static upscli_authconf_t *current_section = NULL; +static upscli_authconf_t *global_defaults = NULL; +static int current_section_with_fixed_username = 0; + +upscli_authconf_t *upscli_get_authconf_list(void) +{ + return authconf_list; +} + +static upscli_authconf_t *upscli_add_authconf(const char *section) +{ + upscli_authconf_t *node = xcalloc(1, sizeof(upscli_authconf_t)); + + if (section) { + node->section = xstrdup(section); + } + node->certverify = -1; + node->forcessl = -1; + + /* Append to 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; +} + +upscli_authconf_t *upscli_free_authconf(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); + + return next; + } + + return NULL; +} + +int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node) +{ + if (!node) + return -1; + + if (!stream) + stream = stdout; + + return fprintf(stream, + "[%s]\n\tUSER = \"%s\"\n\tPASS = \"%s\"\n" + "\tCERTPATH = \"%s\"\n\tCERTFILE = \"%s\"\n" + "\tCERTIDENT_NAME = \"%s\"\n\tCERTIDENT_PASS = \"%s\"\n" + "\tSSLBACKEND = \"%s\"\n" + "\tCERTVERIFY = %i\n\tFORCESSL = %i\n\n", + NUT_STRARG(node->section), + NUT_STRARG(node->user), + NUT_STRARG(node->pass), + NUT_STRARG(node->certpath), + NUT_STRARG(node->certfile), + NUT_STRARG(node->certident), + NUT_STRARG(node->certpasswd), + NUT_STRARG(node->ssl_backend), + node->certverify, + node->forcessl + ); +} + +size_t upscli_dump_authconf_list(FILE *restrict stream) +{ + upscli_authconf_t *node = authconf_list; + size_t count = 0; + + while (node) { + count++; + upscli_dump_authconf(stream, node); + node = node->next; + } + + return count; +} + +void upscli_free_authconf_list(void) +{ + upscli_authconf_t *node = authconf_list; + + while (node) { + node = upscli_free_authconf(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")) { + 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, "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); +} + +static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope); + +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, *at = NULL, *colon = NULL, *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; + char normalized_sect_name[LARGEBUF]; + upscli_authconf_t *tmp = NULL; + + if (!global_scope) { + upslogx(LOG_WARNING, "Section header ignored in included file with section-scope"); + return; + } + + sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ + sect_name[strlen(sect_name)-1] = '\0'; /* forget trailing ']' */ + + at = strchr(sect_name, '@'); + colon = strchr(sect_name, ':'); + if (at && colon && colon < at) { + fatalx(LOG_WARNING, "Invalid section header: colon ':' before at '@'"); + } + + current_section_with_fixed_username = (at && at != sect_name); + if (current_section_with_fixed_username) { + /* If section matched user@host:port, ensure user is set to this user */ + sect_user = xstrdup(sect_name); + sect_user[at - sect_name] = '\0'; + } + + if (at) { + if (at + 1 != colon) { + sect_host = xstrdup(at + 1); + if (colon) { + sect_host[colon - at - 1] = '\0'; + } + } /* else keep NULL */ + } else { + if (sect_name + 1 != colon) { + sect_host = xstrdup(sect_name); + if (colon) { + sect_host[colon - at - 1] = '\0'; + } + } /* else keep NULL */ + } + + if (colon && colon[1]) { + sect_port = xstrdup(colon + 1); + } + + if (!sect_host || !*sect_host) + sect_host = xstrdup("localhost"); + + if (!sect_port || !*sect_port) { + sect_port = xcalloc(6, sizeof(char)); + snprintf(sect_port, 6, "%u", NUT_PORT); + } + + snprintf(normalized_sect_name, sizeof(normalized_sect_name), "%s@%s:%s", + sect_user ? sect_user : "", + sect_host, + sect_port); + + /* Find if section already exists */ + tmp = authconf_list; + current_section = NULL; + 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(normalized_sect_name); + + if (current_section_with_fixed_username && sect_user && *sect_user) { + /* If section matched user@host:port, ensure user is set to this user */ + current_section->user = xstrdup(sect_user); + } + + /* Copy global defaults to new section */ + if (global_defaults) { + if (!(current_section->user) && global_defaults->user) current_section->user = xstrdup(global_defaults->user); + if (global_defaults->pass) current_section->pass = xstrdup(global_defaults->pass); + if (global_defaults->certpath) current_section->certpath = xstrdup(global_defaults->certpath); + if (global_defaults->certfile) current_section->certfile = xstrdup(global_defaults->certfile); + if (global_defaults->certident) current_section->certident = xstrdup(global_defaults->certident); + if (global_defaults->certpasswd) current_section->certpasswd = xstrdup(global_defaults->certpasswd); + if (global_defaults->ssl_backend) current_section->ssl_backend = xstrdup(global_defaults->ssl_backend); + current_section->certverify = global_defaults->certverify; + current_section->forcessl = global_defaults->forcessl; + } + } + + free(sect_name); + free(sect_user); + free(sect_host); + free(sect_port); + 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], "=")) { + val = arg[2]; + } else if (numargs == 1) { + /* Flag property? */ + val = "1"; + } + + if (current_section) { + set_authconf_val(current_section, var, val); + } else { + /* Modifying global defaults */ + if (!global_defaults) { + global_defaults = upscli_add_authconf(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; + + 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); + } + + pconf_finish(&ctx); + return 1; +} + +int upscli_read_authconf(const char *filename, int fatal_errors) +{ + char fn[NUT_PATH_MAX + 1]; + + if (!filename) { + snprintf(fn, sizeof(fn), "%s/nutauth.conf", confpath()); + filename = fn; + } + + /* Ensure we start fresh if called multiple times */ + upscli_free_authconf_list(); + + return parse_authconf_file(filename, fatal_errors, 1); +} + +upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port) +{ + char target_user_host_port[LARGEBUF]; + char target_host_port[SMALLBUF]; + + if (!host && !port && !user) { + /* Global section only */ + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + if (!tmp->section || !*(tmp->section)) { + return tmp; + } + tmp = tmp->next; + } + return NULL; + } + + if (host && port && *host && *port) { + snprintf(target_host_port, sizeof(target_host_port), "@%s:%s", host, port); + } else if (host && *host) { + snprintf(target_host_port, sizeof(target_host_port), "@%s:%u", host, (unsigned int)NUT_PORT); + } else if (port && *port) { + snprintf(target_host_port, sizeof(target_host_port), "@localhost:%s", port); + } else { + snprintf(target_host_port, sizeof(target_host_port), "@localhost:%u", (unsigned int)NUT_PORT); + } + + snprintf(target_user_host_port, sizeof(target_user_host_port), "%s%s", + ((user && *user) ? user : ""), + target_host_port /* Note: includes the '@' */ + ); + + /* 1. Try exact user@host:port */ + if (target_user_host_port[0]) { + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + upsdebugx(2, "%s: matching '%s' against '%s'", __func__, target_user_host_port, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, target_user_host_port)) { + return tmp; + } + tmp = tmp->next; + } + } + + /* 2. Try @host:port (host defaults) */ + if (target_host_port[0]) { + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + upsdebugx(2, "%s: matching '%s' against '%s'", __func__, target_host_port, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, target_host_port)) { + return tmp; + } + tmp = tmp->next; + } + } + + /* 3. Global defaults (section == NULL) */ + return global_defaults; +} diff --git a/clients/authconf.h b/clients/authconf.h new file mode 100644 index 0000000000..c574a80c42 --- /dev/null +++ b/clients/authconf.h @@ -0,0 +1,69 @@ +/* authconf.h - prototypes and structures for NUT client authentication configuration + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef NUT_AUTHCONF_H_SEEN +#define NUT_AUTHCONF_H_SEEN 1 + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "nut_stdint.h" + +typedef struct upscli_authconf_s { + char *section; /* [@host:port] or [user@host:port], or NULL for global */ + char *user; + char *pass; + char *certpath; + char *certfile; + char *certident; + char *certpasswd; /* Password for key/cert storage */ + char *ssl_backend; /* openssl/nss */ + 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 list of all parsed authentication configurations */ +upscli_authconf_t *upscli_get_authconf_list(void); + +/** Free an authentication configuration item (if not NULL) and return its "next" pointer */ +upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); + +/** Free the list of authentication configurations */ +void upscli_free_authconf_list(void); + +/** Read the authentication configuration file (usually nutauth.conf) + * returns -1 on error, 1 on success + */ +int upscli_read_authconf(const char *filename, int fatal_errors); + +/** Find the best matching authconf for a given connection string; + * if all args are NULL, return the global section or NULL if none such in the list. + */ +upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port); + +/** Print ultimate configuration to specified stream (stdout if NULL) + * and return the number of nodes in the current authconf list */ +size_t upscli_dump_authconf_list(FILE *restrict stream); + +/** Print one node to specified stream (stdout if NULL), return fprintf() return code; + * used from upscli_dump_authconf_list() */ +int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node); + +#ifdef __cplusplus +} +#endif + +#endif /* NUT_AUTHCONF_H_SEEN */ diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index abfc622420..ad192b06d8 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -245,6 +245,7 @@ check-list-SRC_ALL_PAGES: # Base configuration and client manpages, always installed SRC_CONF_PAGES = \ nut.conf.txt \ + nutauth.conf.txt \ ups.conf.txt \ upsd.conf.txt \ upsd.users.txt \ @@ -253,6 +254,7 @@ SRC_CONF_PAGES = \ INST_MAN_CONF_PAGES = \ nut.conf.$(MAN_SECTION_CFG) \ + nutauth.conf.$(MAN_SECTION_CFG) \ ups.conf.$(MAN_SECTION_CFG) \ upsd.conf.$(MAN_SECTION_CFG) \ upsd.users.$(MAN_SECTION_CFG) \ @@ -269,6 +271,7 @@ mancfg_DATA += $(MAN_CONF_PAGES) INST_HTML_CONF_MANS = \ nut.conf.html \ + nutauth.conf.html \ ups.conf.html \ upsd.conf.html \ upsd.users.html \ @@ -553,6 +556,10 @@ SRC_DEV_PAGES = \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_upslog_set_debug_level.txt \ + upscli_get_authconf_list.txt \ + upscli_read_authconf.txt \ + upscli_find_authconf.txt \ + upscli_dump_authconf_list.txt \ upscli_str_add_unique_token.txt \ upscli_str_contains_token.txt \ libnutclient.txt \ @@ -749,6 +756,13 @@ INST_MAN_DEV_API_PAGES = \ upscli_strerror.$(MAN_SECTION_API) \ upscli_upserror.$(MAN_SECTION_API) \ upscli_upslog_set_debug_level.$(MAN_SECTION_API) \ + upscli_get_authconf_list.$(MAN_SECTION_API) \ + upscli_free_authconf_list.$(MAN_SECTION_API) \ + upscli_read_authconf.$(MAN_SECTION_API) \ + upscli_find_authconf.$(MAN_SECTION_API) \ + upscli_free_authconf.$(MAN_SECTION_API) \ + upscli_dump_authconf_list.$(MAN_SECTION_API) \ + upscli_dump_authconf.$(MAN_SECTION_API) \ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS) \ upscli_str_add_unique_token.$(MAN_SECTION_API) \ upscli_str_contains_token.$(MAN_SECTION_API) \ @@ -816,6 +830,15 @@ upscli_sendline_timeout_may_disconnect.$(MAN_SECTION_API): upscli_sendline.$(MAN upscli_tryconnect.$(MAN_SECTION_API): upscli_connect.$(MAN_SECTION_API) touch $@ +upscli_free_authconf_list.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) + touch $@ + +upscli_free_authconf.$(MAN_SECTION_API): upscli_find_authconf.$(MAN_SECTION_API) + touch $@ + +upscli_dump_authconf.$(MAN_SECTION_API): upscli_dump_authconf_list.$(MAN_SECTION_API) + touch $@ + nutscan_scan_ip_range_snmp.$(MAN_SECTION_API): nutscan_scan_snmp.$(MAN_SECTION_API) touch $@ @@ -890,6 +913,10 @@ INST_HTML_DEV_MANS = \ upscli_strerror.html \ upscli_upserror.html \ upscli_upslog_set_debug_level.html \ + upscli_get_authconf_list.html \ + upscli_read_authconf.html \ + upscli_find_authconf.html \ + upscli_dump_authconf_list.html \ upscli_str_add_unique_token.html \ upscli_str_contains_token.html \ libnutclient.html \ @@ -957,6 +984,15 @@ 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_free_authconf_list.html: upscli_get_authconf_list.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_free_authconf.html: upscli_find_authconf.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_dump_authconf.html: upscli_dump_authconf_list.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + nutscan_scan_ip_range_snmp.html: nutscan_scan_snmp.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt new file mode 100644 index 0000000000..fd0afe191d --- /dev/null +++ b/docs/man/nutauth.conf.txt @@ -0,0 +1,152 @@ +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[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. + +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. + +*[user@host]*:: + Defines credentials and settings for a specific user on a specific + host (uses the default NUT port). + +An empty 'host' component value is interpreted as `localhost`, e.g. in `[@:port]`. + +A section continues until the next section header or until EOF. + +The special name `[default]` is reserved and should not be used as a section +header; use the global scope (before any section header) for default settings. + +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* (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. + +*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. + +Example: + + INCLUDE_REQUIRED /etc/nut/nutauth-defaults.conf + + [@localhost] + INCLUDE /etc/nut/nutauth-local.conf + +SEE ALSO +-------- + +linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[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/upscli_dump_authconf_list.txt b/docs/man/upscli_dump_authconf_list.txt new file mode 100644 index 0000000000..94edb93851 --- /dev/null +++ b/docs/man/upscli_dump_authconf_list.txt @@ -0,0 +1,44 @@ +UPSCLI_DUMP_AUTHCONF_LIST(3) +============================ + +NAME +---- + +upscli_dump_authconf_list, upscli_dump_authconf - Print authentication configuration list + +SYNOPSIS +-------- + +------ + #include + + size_t upscli_dump_authconf_list(FILE *restrict stream); + + int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node); +------ + +DESCRIPTION +----------- + +The *upscli_dump_authconf_list()* function prints the entire internal list of +authentication configurations to the specified 'stream'. If 'stream' is NULL, +it defaults to `stdout`. + +The *upscli_dump_authconf()* function prints a single configuration 'node' +to the specified 'stream'. + +These functions are primarily intended for debugging purposes to verify the +content of the parsed configuration. + +RETURN VALUE +------------ + +The *upscli_dump_authconf_list()* function returns the number of nodes printed. + +The *upscli_dump_authconf()* function returns the return value of the +underlying linkman:fprintf[3] call, or -1 if 'node' is NULL. + +SEE ALSO +-------- + +linkman:upscli_read_authconf[3], linkman:upscli_get_authconf_list[3] diff --git a/docs/man/upscli_find_authconf.txt b/docs/man/upscli_find_authconf.txt new file mode 100644 index 0000000000..1ddc728ae8 --- /dev/null +++ b/docs/man/upscli_find_authconf.txt @@ -0,0 +1,57 @@ +UPSCLI_FIND_AUTHCONF(3) +======================= + +NAME +---- + +upscli_find_authconf, upscli_free_authconf - Find and free authentication configuration items + +SYNOPSIS +-------- + +------ + #include + + upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port); + + upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); +------ + +DESCRIPTION +----------- + +The *upscli_find_authconf()* function searches the internal list of +authentication configurations for the best match for the given 'user', +'host', and 'port'. + +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) + +If a specific match is found, any missing fields in that section are +inherited from the global defaults. + +The *upscli_free_authconf()* 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_find_authconf()* 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()* function returns the last known value of the `next` +pointer field from the node being freed. + +SEE ALSO +-------- + +linkman:upscli_read_authconf[3], linkman:upscli_get_authconf_list[3], +linkman:upscli_free_authconf_list[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..9294ce64c2 --- /dev/null +++ b/docs/man/upscli_get_authconf_list.txt @@ -0,0 +1,61 @@ +UPSCLI_GET_AUTHCONF_LIST(3) +=========================== + +NAME +---- + +upscli_get_authconf_list, upscli_free_authconf_list - Get and free the list of authentication configurations + +SYNOPSIS +-------- + +------ + #include + + upscli_authconf_t *upscli_get_authconf_list(void); + + void upscli_free_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[3]. + +Each element in the list is of type `upscli_authconf_t`: + +[source,c] +---------- +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 */ + int certverify; /* -1 = unset, 0 = off, 1 = on */ + int forcessl; /* -1 = unset, 0 = off, 1 = on */ + + struct upscli_authconf_s *next; +} upscli_authconf_t; +---------- + +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_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[3]. + +SEE ALSO +-------- + +linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[3], +linkman:upscli_free_authconf[3], linkman:upscli_dump_authconf_list[3] diff --git a/docs/man/upscli_read_authconf.txt b/docs/man/upscli_read_authconf.txt new file mode 100644 index 0000000000..b5db96884d --- /dev/null +++ b/docs/man/upscli_read_authconf.txt @@ -0,0 +1,58 @@ +UPSCLI_READ_AUTHCONF(3) +======================= + +NAME +---- + +upscli_read_authconf - Read the authentication configuration file + +SYNOPSIS +-------- + +------ + #include + + int upscli_read_authconf(const char *filename, int fatal_errors); +------ + +DESCRIPTION +----------- + +The *upscli_read_authconf()* function reads the specified 'filename' (which +is usually the path to *nutauth.conf*) and populates an internal list of +authentication and SSL configurations. + +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()* 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[3], +linkman:upscli_dump_authconf_list[3], linkman:nutauth.conf[5] diff --git a/docs/nut.dict b/docs/nut.dict index e9ecc757d3..12fead72a2 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -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 diff --git a/tests/Makefile.am b/tests/Makefile.am index 2bdab40017..2233d9fe4c 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,11 @@ 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_CFLAGS = $(AM_CFLAGS) -I$(top_srcdir)/clients + # Separate the .deps of other dirs from this one LINKED_SOURCE_FILES = hidparser.c diff --git a/tests/test_authconf.c b/tests/test_authconf.c new file mode 100644 index 0000000000..eb45aafdc4 --- /dev/null +++ b/tests/test_authconf.c @@ -0,0 +1,220 @@ +/* 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 "authconf.h" + +#include +#include +#include +#include +#include + +/* Mocks for functions usually provided by libcommon */ +void s_upsdebugx(int level, const char *fmt, ...) { + va_list ap; + + /*if (level > 1) return;*/ + fprintf(stderr, "[D%d] ", level); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, "\n"); +} + +void s_upslogx(int priority, const char *fmt, ...) { + va_list ap; + + fprintf(stderr, "[PRIO:%d] ", priority); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, "\n"); +} + +void fatalx(int exitcode, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, "\n"); + exit(exitcode); +} + +void *xcalloc(size_t nmemb, size_t size) { + void *p = calloc(nmemb, size); + + if (!p) fatalx(EXIT_FAILURE, "out of memory"); + + return p; +} + +char *xstrdup(const char *s) { + char *p = strdup(s); + + if (!p) fatalx(EXIT_FAILURE, "out of memory"); + + return p; +} + +const char *confpath(void) { + return "."; +} + +void set_close_on_exec(int fd) { + /* mock */ + if (fd) return; +} + +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; + size_t num_sections; + char buf[512]; + + if (argc > 1) { + s_upsdebugx(1, "Args ignored: '%s' etc.", argv[0]); + } + + /* Create dummy config files */ + f = fopen(test_conf, "w"); + if (!f) { + perror("fopen test_nutauth.conf"); + return 1; + } + fprintf(f, "USER = globaluser\n"); + fprintf(f, "PASS = globalpass\n"); + fprintf(f, "CERTVERIFY = 1\n"); + fprintf(f, "INCLUDE %s\n", include_conf); + fprintf(f, "[@localhost:12345]\n"); + fprintf(f, " USER = hostuser\n"); + fprintf(f, " FORCESSL = 1\n"); + fprintf(f, "[admin@localhost:12345]\n"); + fprintf(f, " PASS = adminpass\n"); + fprintf(f, " FORCESSL = 1\n"); + fclose(f); + + f = fopen(include_conf, "w"); + if (!f) { + perror("fopen test_include.conf"); + return 1; + } + fprintf(f, "[@otherhost]\n"); + fprintf(f, " USER = otheruser\n"); + fclose(f); + + if (upscli_read_authconf(test_conf, 1) != 1) { + fprintf(stderr, "read_authconf failed\n"); + return 1; + } + + printf("=== Parsed configuration:\n"); + num_sections = upscli_dump_authconf_list(NULL); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + + /* Test matching */ + printf("=== Testing matches...\n"); + + /* 1. Global match (no specific section for this host) */ + printf("Checking global match for '@somehost:port'...\n"); + ac = upscli_find_authconf(NULL, "somehost", "port"); + if (ac) { + printf("Global match got user=%s\n", ac->user ? ac->user : "NULL"); + if (ac->user && strcmp(ac->user, "globaluser") == 0) { + printf("Global match OK\n"); + } else { + printf("Global match FAILED (wrong user)\n"); + return 1; + } + } else { + printf("Global match FAILED (no ac)\n"); + return 1; + } + + /* 2. Host default match */ + printf("Checking host default match for '@localhost:12345'\n"); + ac = upscli_find_authconf(NULL, "localhost", "12345"); + if (ac && strcmp(ac->user, "hostuser") == 0 && ac->forcessl == 1 && ac->certverify == 1) { + printf("Host default match OK\n"); + } else { + printf("Host default match FAILED\n"); + return 1; + } + + /* 3. Exact match */ + printf("Checking exact match for 'admin@localhost:12345'\n"); + ac = upscli_find_authconf("admin", "localhost", "12345"); + 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("Exact match OK\n"); + } else { + printf("Exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", "admin", "adminpass"); + return 1; + } + } else { + printf("Exact match FAILED (no ac)\n"); + return 1; + } + + /* 4. Include match */ + printf("Checking include match for '@otherhost'\n"); + ac = upscli_find_authconf(NULL, "otherhost", NULL); + 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 + ) { + printf("Include match OK\n"); + } else { + if (ac) { + printf("Include match FAILED: got section=%s user=%s\n", + ac->section ? ac->section : "NULL", + ac->user ? ac->user : "NULL"); + } else { + printf("Include match FAILED: no ac\n"); + } + return 1; + } + + /* 5. No bogus hits */ + printf("Checking NO match for '@otherhost:portnum' other than global section\n"); + ac = upscli_find_authconf(NULL, "otherhost", "portnum"); + if (ac) { + if (!(ac->section) || !*(ac->section)) { + printf("No bogus match OK: got global section\n"); + } else { + printf("No bogus match FAILED: had a hit\n"); + upscli_dump_authconf(NULL, ac); + return 1; + } + } else { + printf("No bogus match kind of OK: got no ac\n"); + } + + upscli_free_authconf_list(); + unlink(test_conf); + unlink(include_conf); + + printf("All tests passed!\n"); + return 0; +} From 39ce4fdb7e6b7e17d01533bb304034d932c1a440 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 08:55:54 +0200 Subject: [PATCH 002/108] tests/test_authconf.c: drop "Mocks for functions usually provided by libcommon" [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 60 ++----------------------------------------- 1 file changed, 2 insertions(+), 58 deletions(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index eb45aafdc4..23012a45e0 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -10,6 +10,7 @@ #include "config.h" +#include "common.h" #include "authconf.h" #include @@ -18,63 +19,6 @@ #include #include -/* Mocks for functions usually provided by libcommon */ -void s_upsdebugx(int level, const char *fmt, ...) { - va_list ap; - - /*if (level > 1) return;*/ - fprintf(stderr, "[D%d] ", level); - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fprintf(stderr, "\n"); -} - -void s_upslogx(int priority, const char *fmt, ...) { - va_list ap; - - fprintf(stderr, "[PRIO:%d] ", priority); - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fprintf(stderr, "\n"); -} - -void fatalx(int exitcode, const char *fmt, ...) { - va_list ap; - - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fprintf(stderr, "\n"); - exit(exitcode); -} - -void *xcalloc(size_t nmemb, size_t size) { - void *p = calloc(nmemb, size); - - if (!p) fatalx(EXIT_FAILURE, "out of memory"); - - return p; -} - -char *xstrdup(const char *s) { - char *p = strdup(s); - - if (!p) fatalx(EXIT_FAILURE, "out of memory"); - - return p; -} - -const char *confpath(void) { - return "."; -} - -void set_close_on_exec(int fd) { - /* mock */ - if (fd) return; -} - int main(int argc, char **argv) { const char *test_conf = "test_nutauth.conf"; @@ -85,7 +29,7 @@ int main(int argc, char **argv) char buf[512]; if (argc > 1) { - s_upsdebugx(1, "Args ignored: '%s' etc.", argv[0]); + upsdebugx(1, "Args ignored: '%s' etc.", argv[0]); } /* Create dummy config files */ From 8c252d24a29403a850da397147c5a26d92fa5094 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 11:18:51 +0200 Subject: [PATCH 003/108] clients/authconf.{c,h}, tests, docs: extend upscli_dump_authconf{,_list} methods with "for_debug" option [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 143 +++++++++++++++++++++---- clients/authconf.h | 19 ++-- docs/man/upscli_dump_authconf_list.txt | 19 +++- tests/test_authconf.c | 12 ++- 4 files changed, 162 insertions(+), 31 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 5668789111..d84217dddd 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -74,41 +74,144 @@ upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node) return NULL; } -int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node) +static int upscli_dump_authconf_line_str(FILE *restrict stream, const char *var, const char *val, const char *indent, int for_debug) { + /* Assume sane inputs from upscli_dump_authconf(); 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 *restrict stream, const char *var, int val, const char *indent, int for_debug) +{ + /* Assume sane inputs from upscli_dump_authconf(); 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(FILE *restrict stream, upscli_authconf_t *node, int for_debug) +{ + char *indent = NULL; + int res = 0, ret = 0; + if (!node) return -1; if (!stream) stream = stdout; - return fprintf(stream, - "[%s]\n\tUSER = \"%s\"\n\tPASS = \"%s\"\n" - "\tCERTPATH = \"%s\"\n\tCERTFILE = \"%s\"\n" - "\tCERTIDENT_NAME = \"%s\"\n\tCERTIDENT_PASS = \"%s\"\n" - "\tSSLBACKEND = \"%s\"\n" - "\tCERTVERIFY = %i\n\tFORCESSL = %i\n\n", - NUT_STRARG(node->section), - NUT_STRARG(node->user), - NUT_STRARG(node->pass), - NUT_STRARG(node->certpath), - NUT_STRARG(node->certfile), - NUT_STRARG(node->certident), - NUT_STRARG(node->certpasswd), - NUT_STRARG(node->ssl_backend), - node->certverify, - node->forcessl - ); + 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", 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", 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_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 *restrict stream) +size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug) { upscli_authconf_t *node = authconf_list; size_t count = 0; while (node) { count++; - upscli_dump_authconf(stream, node); + upscli_dump_authconf(stream, node, for_debug); node = node->next; } diff --git a/clients/authconf.h b/clients/authconf.h index c574a80c42..63743aac0d 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -54,13 +54,20 @@ int upscli_read_authconf(const char *filename, int fatal_errors); */ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port); -/** Print ultimate configuration to specified stream (stdout if NULL) - * and return the number of nodes in the current authconf list */ -size_t upscli_dump_authconf_list(FILE *restrict stream); +/** 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(FILE *restrict stream, upscli_authconf_t *node, int for_debug); -/** Print one node to specified stream (stdout if NULL), return fprintf() return code; - * used from upscli_dump_authconf_list() */ -int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node); +/** 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 *restrict stream, int for_debug); #ifdef __cplusplus } diff --git a/docs/man/upscli_dump_authconf_list.txt b/docs/man/upscli_dump_authconf_list.txt index 94edb93851..3eefae6833 100644 --- a/docs/man/upscli_dump_authconf_list.txt +++ b/docs/man/upscli_dump_authconf_list.txt @@ -12,9 +12,9 @@ SYNOPSIS ------ #include - size_t upscli_dump_authconf_list(FILE *restrict stream); + size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug); - int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node); + int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node, int for_debug); ------ DESCRIPTION @@ -30,6 +30,21 @@ to the specified 'stream'. These functions are primarily intended for debugging purposes to verify the content of the parsed configuration. +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). + RETURN VALUE ------------ diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 23012a45e0..d1436f8d40 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -64,8 +64,14 @@ int main(int argc, char **argv) return 1; } - printf("=== Parsed configuration:\n"); - num_sections = upscli_dump_authconf_list(NULL); + 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); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + + printf("=== Parsed configuration (debug view):\n"); + /* With "for_debug", show all fields (highlight NULLs) */ + num_sections = upscli_dump_authconf_list(NULL, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); /* Test matching */ @@ -148,7 +154,7 @@ int main(int argc, char **argv) printf("No bogus match OK: got global section\n"); } else { printf("No bogus match FAILED: had a hit\n"); - upscli_dump_authconf(NULL, ac); + upscli_dump_authconf(NULL, ac, 1); return 1; } } else { From e871d8154712efb180d4b288c0d664edacbf4274 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 12:33:18 +0200 Subject: [PATCH 004/108] clients/authconf.{c,h}, docs/man/upscli_find_authconf.txt: refactor with upscli_split_auth_section() [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 178 ++++++++++++++++++++++-------- clients/authconf.h | 12 ++ docs/man/Makefile.am | 7 ++ docs/man/upscli_find_authconf.txt | 22 +++- docs/nut.dict | 3 +- 5 files changed, 174 insertions(+), 48 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index d84217dddd..f5c538e56e 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -287,6 +287,130 @@ static void authconf_err(const char *errmsg) upslogx(LOG_ERR, "Error in parseconf(authconf): %s", errmsg); } +int upscli_split_auth_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port) +{ + /* Take raw sect_name as input (e.g. a user-written string from config files). + * Normalize it by splitting into user, host, and port components (populating absent values). + * Return normalized components and reconstructed section name in output parameters (if not NULL). + */ + const char *at = NULL, *colon = NULL; + char *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; + int fixed_sect_user = 0; + + if (!sect_name) { + upsdebugx(1, "%s: sect_name is NULL", __func__); + return -1; + } + + at = strchr(sect_name, '@'); + colon = strchr(sect_name, ':'); + if (at && colon && colon < at) { + upsdebugx(1, "%s: Invalid section header: colon ':' before at '@': '%s'", __func__, sect_name); + return -1; + } + + fixed_sect_user = (at && at != sect_name); + if (fixed_sect_user) { + /* If section matched user@host:port, ensure user is set to this user */ + sect_user = xstrdup(sect_name); + if (!sect_user) goto failed; + sect_user[at - sect_name] = '\0'; + } + + 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 NULL */ + } else { + 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 NULL */ + } + + if (colon && colon[1]) { + /* FIXME: As port is a string, resolve it (if not a number, + * try to get one via "services" naming database) */ + sect_port = xstrdup(colon + 1); + if (!sect_port) goto failed; + } + + if (!sect_host || !*sect_host) { + free(sect_host); + sect_host = xstrdup("localhost"); + if (!sect_host) goto failed; + } + + if (!sect_port || !*sect_port) { + free(sect_port); + sect_port = xcalloc(6, sizeof(char)); + if (!sect_port) goto failed; + if (snprintf(sect_port, 6, "%u", NUT_PORT) < 1) { + upsdebugx(1, "%s: Failed to construct default port number", __func__); + goto failed; + } + } + + /* Only now that we (almost) do not expect failures, we can + * consistently populate caller's output variables (if any) */ + if (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 + ) { + *normalized_sect_name = xstrdup(normalized_sect_name_buf); + } else { + upsdebugx(1, "%s: Failed to reconstruct normalized section header from '%s'", __func__, sect_name); + 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 int parse_authconf_file(const char *filename, int fatal_errors, int global_scope); static void handle_authconf_args(size_t numargs, char **arg, int global_scope) @@ -299,8 +423,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) /* Section header [section] */ if (arg[0][0] == '[' && arg[0][strlen(arg[0])-1] == ']') { - char *sect_name = NULL, *at = NULL, *colon = NULL, *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; - char normalized_sect_name[LARGEBUF]; + char *sect_name = NULL, *sect_user = NULL, *sect_host = NULL, *sect_port = NULL, *normalized_sect_name = NULL; upscli_authconf_t *tmp = NULL; if (!global_scope) { @@ -311,52 +434,13 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ sect_name[strlen(sect_name)-1] = '\0'; /* forget trailing ']' */ - at = strchr(sect_name, '@'); - colon = strchr(sect_name, ':'); - if (at && colon && colon < at) { - fatalx(LOG_WARNING, "Invalid section header: colon ':' before at '@'"); - } - - current_section_with_fixed_username = (at && at != sect_name); - if (current_section_with_fixed_username) { - /* If section matched user@host:port, ensure user is set to this user */ - sect_user = xstrdup(sect_name); - sect_user[at - sect_name] = '\0'; - } - - if (at) { - if (at + 1 != colon) { - sect_host = xstrdup(at + 1); - if (colon) { - sect_host[colon - at - 1] = '\0'; - } - } /* else keep NULL */ - } else { - if (sect_name + 1 != colon) { - sect_host = xstrdup(sect_name); - if (colon) { - sect_host[colon - at - 1] = '\0'; - } - } /* else keep NULL */ - } - - if (colon && colon[1]) { - sect_port = xstrdup(colon + 1); - } - - if (!sect_host || !*sect_host) - sect_host = xstrdup("localhost"); - - if (!sect_port || !*sect_port) { - sect_port = xcalloc(6, sizeof(char)); - snprintf(sect_port, 6, "%u", NUT_PORT); + if (upscli_split_auth_section(sect_name, &normalized_sect_name, + §_user, ¤t_section_with_fixed_username, + §_host, §_port) < 0 + ) { + fatalx(EXIT_FAILURE, "Invalid nutauth section header: %s", NUT_STRARG(sect_name)); } - snprintf(normalized_sect_name, sizeof(normalized_sect_name), "%s@%s:%s", - sect_user ? sect_user : "", - sect_host, - sect_port); - /* Find if section already exists */ tmp = authconf_list; current_section = NULL; @@ -390,6 +474,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) } } + free(normalized_sect_name); free(sect_name); free(sect_user); free(sect_host); @@ -516,6 +601,7 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons return NULL; } + /* FIXME? Unite somehow with upscli_split_auth_section()? */ if (host && port && *host && *port) { snprintf(target_host_port, sizeof(target_host_port), "@%s:%s", host, port); } else if (host && *host) { diff --git a/clients/authconf.h b/clients/authconf.h index 63743aac0d..6c8e8dea72 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -49,6 +49,18 @@ void upscli_free_authconf_list(void); */ int upscli_read_authconf(const char *filename, int fatal_errors); +/** 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_auth_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; * if all args are NULL, return the global section or NULL if none such in the list. */ diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index ad192b06d8..71078f7ac1 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -757,6 +757,7 @@ INST_MAN_DEV_API_PAGES = \ upscli_upserror.$(MAN_SECTION_API) \ upscli_upslog_set_debug_level.$(MAN_SECTION_API) \ upscli_get_authconf_list.$(MAN_SECTION_API) \ + upscli_split_auth_section.$(MAN_SECTION_API) \ upscli_free_authconf_list.$(MAN_SECTION_API) \ upscli_read_authconf.$(MAN_SECTION_API) \ upscli_find_authconf.$(MAN_SECTION_API) \ @@ -830,6 +831,9 @@ upscli_sendline_timeout_may_disconnect.$(MAN_SECTION_API): upscli_sendline.$(MAN upscli_tryconnect.$(MAN_SECTION_API): upscli_connect.$(MAN_SECTION_API) touch $@ +upscli_split_auth_section.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) + touch $@ + upscli_free_authconf_list.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) touch $@ @@ -984,6 +988,9 @@ 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_split_auth_section.html: upscli_get_authconf_list.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + upscli_free_authconf_list.html: upscli_get_authconf_list.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ diff --git a/docs/man/upscli_find_authconf.txt b/docs/man/upscli_find_authconf.txt index 1ddc728ae8..3bddb2dc23 100644 --- a/docs/man/upscli_find_authconf.txt +++ b/docs/man/upscli_find_authconf.txt @@ -4,7 +4,8 @@ UPSCLI_FIND_AUTHCONF(3) NAME ---- -upscli_find_authconf, upscli_free_authconf - Find and free authentication configuration items +upscli_find_authconf, upscli_split_auth_section, upscli_free_authconf - Find, +parse name, and free authentication configuration items SYNOPSIS -------- @@ -14,6 +15,13 @@ SYNOPSIS upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port); + int upscli_split_auth_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); + upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); ------ @@ -33,6 +41,18 @@ The matching logic follows this priority: If a specific match is found, any missing fields in that section are inherited from the global defaults. +The "*upscli_split_auth_section()*" function splits a `sect_name` which may be +from a user-typed configuration file into user, host and port sections, and +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). Resulting normalized values +are returned to caller using pointers provided in the arguments (if not `NULL`). +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. + The *upscli_free_authconf()* 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 diff --git a/docs/nut.dict b/docs/nut.dict index 12fead72a2..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 @@ -3314,6 +3314,7 @@ spectype spellcheck spellchecked splitaddr +splitauth splitname sprintf squasher From f19209b07bf5a85b7a88faf27daa06806d0d668a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 13:30:40 +0200 Subject: [PATCH 005/108] common/common.c et al: relocate check_perms() from upsd to common code base [#3359] Signed-off-by: Jim Klimov --- common/common.c | 26 +++++++++++++++++++++++--- include/common.h | 3 +++ server/upsd.c | 22 ---------------------- server/upsd.h | 2 -- server/user.h | 3 --- 5 files changed, 26 insertions(+), 30 deletions(-) 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/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/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* */ } From 95519e8f7cbd25a4753394dad03fa3906de5b8ab Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 13:33:02 +0200 Subject: [PATCH 006/108] clients/authconf.c: parse_authconf_file(): call check_perms(filename) [#3329, #3359] Signed-off-by: Jim Klimov --- clients/authconf.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index f5c538e56e..66820a2927 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -23,6 +23,8 @@ static upscli_authconf_t *current_section = NULL; static upscli_authconf_t *global_defaults = NULL; static int current_section_with_fixed_username = 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; @@ -411,8 +413,6 @@ int upscli_split_auth_section(const char *sect_name, return -1; } -static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope); - static void handle_authconf_args(size_t numargs, char **arg, int global_scope) { /* Property: var = val */ @@ -540,6 +540,8 @@ static int parse_authconf_file(const char *filename, int fatal_errors, int globa { PCONF_CTX_t ctx; + check_perms(filename); + if (!pconf_init(&ctx, authconf_err)) { if (fatal_errors) { exit(EXIT_FAILURE); From 9afa1f5508b24c69a752709ada0643395a4301ce Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 13:59:32 +0200 Subject: [PATCH 007/108] clients/upsclient.{c,h}, docs/man/upscli_init.txt: introduce upscli_init_authconf() [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 15 +++++++++++++++ clients/upsclient.h | 2 ++ docs/man/Makefile.am | 2 +- docs/man/upscli_init.txt | 9 ++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index ed7ba58515..a3381784dd 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -757,6 +757,21 @@ int upscli_init(int certverify, const char *certpath, return upscli_init2(certverify, certpath, certname, certpasswd, NULL); } +/* NOTE: Maybe eventually these two methods below will invert: + * who is implementation of whom. + * TODO: Consider a method that parses our collection from + * upscli_get_authconf_list() to upscli_add_host_cert() and + * set up the one most applicable set of client identity data + * for that [user@host:port] combo. + */ +int upscli_init_authconf(upscli_authconf_t *ac) +{ + if (!ac) + return -1; + + return upscli_init2(ac->certverify, ac->certpath, ac->certident, ac->certpasswd, ac->certfile); +} + int upscli_init2(int certverify, const char *certpath, const char *certname, const char *certpasswd, const char *certfile) diff --git a/clients/upsclient.h b/clients/upsclient.h index 7be5c8b537..221de62a9c 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,6 +153,7 @@ 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); diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 71078f7ac1..73c5e9657f 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -812,7 +812,7 @@ 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 $@ diff --git a/docs/man/upscli_init.txt b/docs/man/upscli_init.txt index 83aed550fa..40375b4c1b 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[3] to pass equivalent information +from linkman:nutauth.conf[5] file(s). + Other nuances ------------- From 8c678723c9009de591b703df19780574518e0cec Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 15:12:25 +0200 Subject: [PATCH 008/108] clients/authconf.{c,h}, docs/man/upscli_read_authconf.txt: add searching for default nutauth.conf in user home or NUT_CONFPATH locations [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 53 ++++++++++++++++++++++++++++--- clients/authconf.h | 5 ++- docs/man/upscli_read_authconf.txt | 6 ++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 66820a2927..dc10502dbb 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -17,6 +17,7 @@ #include #include #include +#include static upscli_authconf_t *authconf_list = NULL; static upscli_authconf_t *current_section = NULL; @@ -575,14 +576,56 @@ int upscli_read_authconf(const char *filename, int fatal_errors) { char fn[NUT_PATH_MAX + 1]; - if (!filename) { - snprintf(fn, sizeof(fn), "%s/nutauth.conf", confpath()); - filename = fn; - } - /* 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; + + 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); } diff --git a/clients/authconf.h b/clients/authconf.h index 6c8e8dea72..d04c35d2b4 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -45,7 +45,10 @@ upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); void upscli_free_authconf_list(void); /** Read the authentication configuration file (usually nutauth.conf) - * returns -1 on error, 1 on success + * 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(const char *filename, int fatal_errors); diff --git a/docs/man/upscli_read_authconf.txt b/docs/man/upscli_read_authconf.txt index b5db96884d..8bdfb5a0ce 100644 --- a/docs/man/upscli_read_authconf.txt +++ b/docs/man/upscli_read_authconf.txt @@ -22,6 +22,12 @@ The *upscli_read_authconf()* 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 tries to locate either a 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. From 74672066fd9ecdd8039f3dab01608313cae93911 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 15:18:21 +0200 Subject: [PATCH 009/108] tests/test_authconf.c: honour NUT_DEBUG_LEVEL [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index d1436f8d40..c862c49b10 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -26,7 +26,15 @@ int main(int argc, char **argv) FILE *f; upscli_authconf_t *ac; size_t num_sections; - char buf[512]; + char buf[512], *s; + int l; + + 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]); From 17e1a86f92eeaaee522a4ade156419bf4e4ae3c3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 15:19:40 +0200 Subject: [PATCH 010/108] tests/test_authconf.c: add a dev-test for discovery of user/site nutauth configs [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index c862c49b10..3d5f764d27 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -67,6 +67,12 @@ int main(int argc, char **argv) fprintf(f, " USER = otheruser\n"); fclose(f); +#ifdef DEBUG + if (upscli_read_authconf(NULL, 0) != 1) { + fprintf(stderr, "INFO: Default read_authconf failed (no user/site-provided config found)\n"); + } +#endif + if (upscli_read_authconf(test_conf, 1) != 1) { fprintf(stderr, "read_authconf failed\n"); return 1; From f3df6e742a10b68224eea4a31c480d78a1349902 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 15:40:20 +0200 Subject: [PATCH 011/108] conf/nutauth.conf.sample.in, configure.ac, conf/Makefile.am, conf/.gitignore: add example config for nutauth.conf [#3329] Signed-off-by: Jim Klimov --- conf/.gitignore | 1 + conf/Makefile.am | 6 ++--- conf/nutauth.conf.sample.in | 48 +++++++++++++++++++++++++++++++++++++ configure.ac | 1 + 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 conf/nutauth.conf.sample.in 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/configure.ac b/configure.ac index 7de682323c..fd5d832d7b 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 From d1fb3d982152d305a8667ecf9238ab46f320aa2d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 16:39:14 +0200 Subject: [PATCH 012/108] scripts/obs/debian.nut-client.install, scripts/obs/debian.nut-client.manpages, scripts/obs/nut.spec: know about nutauth.conf samples [#3329] Signed-off-by: Jim Klimov --- scripts/obs/debian.nut-client.install | 1 + scripts/obs/debian.nut-client.manpages | 1 + scripts/obs/nut.spec | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/obs/debian.nut-client.install b/scripts/obs/debian.nut-client.install index b899f84d60..b5356e1063 100644 --- a/scripts/obs/debian.nut-client.install +++ b/scripts/obs/debian.nut-client.install @@ -6,6 +6,7 @@ debian/tmp/sbin/upsmon debian/tmp/sbin/upssched debian/tmp/bin/upssched-cmd debian/tmp/etc/nut/nut.conf +debian/tmp/etc/nut/nutauth.conf debian/tmp/etc/nut/upsmon.conf debian/tmp/etc/nut/upssched.conf debian/tmp/usr/share/augeas/lenses/dist/nuthostsconf.aug diff --git a/scripts/obs/debian.nut-client.manpages b/scripts/obs/debian.nut-client.manpages index 115e821706..94744f65ce 100644 --- a/scripts/obs/debian.nut-client.manpages +++ b/scripts/obs/debian.nut-client.manpages @@ -4,6 +4,7 @@ debian/tmp/usr/share/man/man8/upsmon.8 debian/tmp/usr/share/man/man8/upsrw.8 debian/tmp/usr/share/man/man8/upssched.8 debian/tmp/usr/share/man/man5/nut.conf.5 +debian/tmp/usr/share/man/man5/nutauth.conf.5 debian/tmp/usr/share/man/man5/upsmon.conf.5 debian/tmp/usr/share/man/man5/upssched.conf.5 debian/tmp/usr/share/man/man8/upslog.8 diff --git a/scripts/obs/nut.spec b/scripts/obs/nut.spec index 158aca99c7..72fbb3c0d4 100644 --- a/scripts/obs/nut.spec +++ b/scripts/obs/nut.spec @@ -573,8 +573,8 @@ bin/chown -R %{NUT_USER} %{STATEPATH} || echo "WARNING: Could not secure state p bin/chgrp -R %{NUT_GROUP} %{STATEPATH} || echo "WARNING: Could not secure state path '%{STATEPATH}'" >&2 # Be sure that all files are owned by a dedicated user. bin/chown %{NUT_USER} %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 -bin/chgrp root %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 -bin/chmod 600 %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 +bin/chgrp root %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users %{CONFPATH}/nutauth.conf || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 +bin/chmod 600 %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users %{CONFPATH}/nutauth.conf || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 # And finally trigger udev to set permissions according to newly installed rules files. if [ -x /sbin/udevadm ] ; then /sbin/udevadm trigger --subsystem-match=usb --property-match=DEVTYPE=usb_device ; fi %if "x%{?systemdtmpfilesdir}" == "x" @@ -691,6 +691,7 @@ if [ -x /sbin/udevadm ] ; then /sbin/udevadm trigger --subsystem-match=usb --pro ### FIXME: if under /etc ### % config(noreplace) % {UDEVRULEPATH}/rules.d/*.rules %{UDEVRULEPATH}/rules.d/*.rules %config(noreplace) %{CONFPATH}/hosts.conf +%config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/nutauth.conf %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsd.conf %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsd.users %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsmon.conf From 3be2a2ca92d5361de2c2c8b49106c19ae2d9779f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 16:44:50 +0200 Subject: [PATCH 013/108] clients/authconf.c: upscli_splitauth(): if "port" is a non-numeric string, try to resolve it in the system naming database [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index dc10502dbb..3a78b45f92 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -18,6 +18,20 @@ #include #include #include +#include + +#ifndef WIN32 +# 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 */ static upscli_authconf_t *authconf_list = NULL; static upscli_authconf_t *current_section = NULL; @@ -344,9 +358,34 @@ int upscli_split_auth_section(const char *sect_name, } if (colon && colon[1]) { - /* FIXME: As port is a string, resolve it (if not a number, - * try to get one via "services" naming database) */ - sect_port = xstrdup(colon + 1); + /* As port is a string, resolve it (if not a number, + * try to get one via "services" naming database) */ + const char *p = colon + 1; + int is_numeric = 1; + + while (*p) { + if (!isdigit((unsigned char)*p)) { + is_numeric = 0; + break; + } + p++; + } + + if (is_numeric) { + sect_port = xstrdup(colon + 1); + } else { + struct servent *se = getservbyname(colon + 1, "tcp"); + + if (se) { + char portbuf[16]; + snprintf(portbuf, sizeof(portbuf), "%u", ntohs(se->s_port)); + sect_port = xstrdup(portbuf); + } else { + /* Resolution failed, fall back to original string */ + sect_port = xstrdup(colon + 1); + } + } + if (!sect_port) goto failed; } From 97a5e09ff1a72e6c3127f68c4447bedc6a9f489a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 16:49:55 +0200 Subject: [PATCH 014/108] clients/authconf.c: upscli_splitauth(): reject empty non-NULL sect_name [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/authconf.c b/clients/authconf.c index 3a78b45f92..9d4054f42d 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -324,6 +324,12 @@ int upscli_split_auth_section(const char *sect_name, return -1; } + if (!(*sect_name)) { + /* TOTHINK: Should this mean `localhost@NUT_PORT`? Or global? Probably neither. */ + upsdebugx(1, "%s: sect_name is empty", __func__); + return -1; + } + at = strchr(sect_name, '@'); colon = strchr(sect_name, ':'); if (at && colon && colon < at) { From 87205aeea2bab2e40d2fa584dbfc90caca6515ea Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 16:55:42 +0200 Subject: [PATCH 015/108] clients/authconf.c: handle_authconf_args(): revise section title line parsing [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 9d4054f42d..e9b0088813 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -470,6 +470,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) /* 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 (!global_scope) { @@ -478,13 +479,23 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) } sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ - sect_name[strlen(sect_name)-1] = '\0'; /* forget trailing ']' */ + end_bracket = strchr(sect_name, ']'); + if (!end_bracket) { + free(sect_name); + fatalx(EXIT_FAILURE, "%s: Invalid section header format: %s", __func__, arg[0]); + } + *(char *)(end_bracket) = '\0'; /* forget trailing ']' and any characters after it (comments etc.) */ if (upscli_split_auth_section(sect_name, &normalized_sect_name, §_user, ¤t_section_with_fixed_username, §_host, §_port) < 0 ) { - fatalx(EXIT_FAILURE, "Invalid nutauth section header: %s", NUT_STRARG(sect_name)); + 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])); } /* Find if section already exists */ From 31a5c5ddb30b097dad61a42055254f59ecd52870 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 12 May 2026 18:03:54 +0200 Subject: [PATCH 016/108] clients/authconf.{c,h}, docs: further refactor with upscli_normalize_auth_section_parts() which we can share and expose [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 179 ++++++++++++++++++++---------- clients/authconf.h | 13 +++ docs/man/Makefile.am | 7 ++ docs/man/upscli_find_authconf.txt | 29 +++-- 4 files changed, 161 insertions(+), 67 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index e9b0088813..7fd021f77c 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -304,6 +304,118 @@ static void authconf_err(const char *errmsg) upslogx(LOG_ERR, "Error in parseconf(authconf): %s", errmsg); } +int upscli_normalize_auth_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_auth_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", 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", 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_auth_section(const char *sect_name, char **normalized_sect_name, char **normalized_sect_user, @@ -364,69 +476,16 @@ int upscli_split_auth_section(const char *sect_name, } if (colon && colon[1]) { - /* As port is a string, resolve it (if not a number, - * try to get one via "services" naming database) */ - const char *p = colon + 1; - int is_numeric = 1; - - while (*p) { - if (!isdigit((unsigned char)*p)) { - is_numeric = 0; - break; - } - p++; - } - - if (is_numeric) { - sect_port = xstrdup(colon + 1); - } else { - struct servent *se = getservbyname(colon + 1, "tcp"); - - if (se) { - char portbuf[16]; - snprintf(portbuf, sizeof(portbuf), "%u", ntohs(se->s_port)); - sect_port = xstrdup(portbuf); - } else { - /* Resolution failed, fall back to original string */ - sect_port = xstrdup(colon + 1); - } - } - + /* May get re-normalized below */ + sect_port = xstrdup(colon + 1); if (!sect_port) goto failed; } - if (!sect_host || !*sect_host) { - free(sect_host); - sect_host = xstrdup("localhost"); - if (!sect_host) goto failed; - } - - if (!sect_port || !*sect_port) { - free(sect_port); - sect_port = xcalloc(6, sizeof(char)); - if (!sect_port) goto failed; - if (snprintf(sect_port, 6, "%u", NUT_PORT) < 1) { - upsdebugx(1, "%s: Failed to construct default port number", __func__); - goto failed; - } - } - - /* Only now that we (almost) do not expect failures, we can - * consistently populate caller's output variables (if any) */ - if (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 - ) { - *normalized_sect_name = xstrdup(normalized_sect_name_buf); - } else { - upsdebugx(1, "%s: Failed to reconstruct normalized section header from '%s'", __func__, sect_name); - goto failed; - } - } + if (upscli_normalize_auth_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; diff --git a/clients/authconf.h b/clients/authconf.h index d04c35d2b4..7f7d802e4f 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -52,6 +52,19 @@ void upscli_free_authconf_list(void); */ int upscli_read_authconf(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_auth_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), diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 73c5e9657f..64d0d527e0 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -757,6 +757,7 @@ INST_MAN_DEV_API_PAGES = \ upscli_upserror.$(MAN_SECTION_API) \ upscli_upslog_set_debug_level.$(MAN_SECTION_API) \ upscli_get_authconf_list.$(MAN_SECTION_API) \ + upscli_normalize_auth_section_parts.$(MAN_SECTION_API) \ upscli_split_auth_section.$(MAN_SECTION_API) \ upscli_free_authconf_list.$(MAN_SECTION_API) \ upscli_read_authconf.$(MAN_SECTION_API) \ @@ -831,6 +832,9 @@ upscli_sendline_timeout_may_disconnect.$(MAN_SECTION_API): upscli_sendline.$(MAN upscli_tryconnect.$(MAN_SECTION_API): upscli_connect.$(MAN_SECTION_API) touch $@ +upscli_normalize_auth_section_parts.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) + touch $@ + upscli_split_auth_section.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) touch $@ @@ -988,6 +992,9 @@ 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_normalize_auth_section_parts.html: upscli_get_authconf_list.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + upscli_split_auth_section.html: upscli_get_authconf_list.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ diff --git a/docs/man/upscli_find_authconf.txt b/docs/man/upscli_find_authconf.txt index 3bddb2dc23..bb84130524 100644 --- a/docs/man/upscli_find_authconf.txt +++ b/docs/man/upscli_find_authconf.txt @@ -4,7 +4,8 @@ UPSCLI_FIND_AUTHCONF(3) NAME ---- -upscli_find_authconf, upscli_split_auth_section, upscli_free_authconf - Find, +upscli_find_authconf, upscli_normalize_auth_section_parts, +upscli_split_auth_section, upscli_free_authconf - Find, parse name, and free authentication configuration items SYNOPSIS @@ -13,7 +14,17 @@ SYNOPSIS ------ #include - upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port); + upscli_authconf_t *upscli_find_authconf( + const char *user, + const char *host, + const char *port); + + int upscli_normalize_auth_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_auth_section(const char *sect_name, char **normalized_sect_name, @@ -43,11 +54,15 @@ inherited from the global defaults. The "*upscli_split_auth_section()*" function splits a `sect_name` which may be from a user-typed configuration file into user, host and port sections, and -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). Resulting normalized values -are returned to caller using pointers provided in the arguments (if not `NULL`). +with "*upscli_normalize_auth_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_auth_section()*", must be not `NULL` in case of +"*upscli_normalize_auth_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 From 10cb5e6890e44445e5daeff093572a1184c7c820 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 00:11:13 +0200 Subject: [PATCH 017/108] clients/authconf.c: refactor upscli_find_authconf() with upscli_normalize_auth_section_parts() [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 87 ++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 7fd021f77c..bb6598d3d0 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -746,11 +746,9 @@ int upscli_read_authconf(const char *filename, int fatal_errors) upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port) { - char target_user_host_port[LARGEBUF]; - char target_host_port[SMALLBUF]; - if (!host && !port && !user) { /* Global section only */ + /* Should we just return global_defaults? */ upscli_authconf_t *tmp = authconf_list; while (tmp) { if (!tmp->section || !*(tmp->section)) { @@ -759,48 +757,61 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons tmp = tmp->next; } return NULL; - } - - /* FIXME? Unite somehow with upscli_split_auth_section()? */ - if (host && port && *host && *port) { - snprintf(target_host_port, sizeof(target_host_port), "@%s:%s", host, port); - } else if (host && *host) { - snprintf(target_host_port, sizeof(target_host_port), "@%s:%u", host, (unsigned int)NUT_PORT); - } else if (port && *port) { - snprintf(target_host_port, sizeof(target_host_port), "@localhost:%s", port); } else { - snprintf(target_host_port, sizeof(target_host_port), "@localhost:%u", (unsigned int)NUT_PORT); - } - - snprintf(target_user_host_port, sizeof(target_user_host_port), "%s%s", - ((user && *user) ? user : ""), - target_host_port /* Note: includes the '@' */ - ); - - /* 1. Try exact user@host:port */ - if (target_user_host_port[0]) { - upscli_authconf_t *tmp = authconf_list; + 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_auth_section_parts( + &normalized_sect_name, + §_user, + &fixed_sect_user, + §_host, + §_port) < 0) + 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(2, "%s: matching '%s' against '%s'", __func__, target_user_host_port, NUT_STRARG(tmp->section)); - if (tmp->section && !strcmp(tmp->section, target_user_host_port)) { - return tmp; + upsdebugx(2, "%s: matching '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, normalized_sect_name)) { + retval = tmp; + goto finished; } tmp = tmp->next; } - } - /* 2. Try @host:port (host defaults) */ - if (target_host_port[0]) { - upscli_authconf_t *tmp = authconf_list; - while (tmp) { - upsdebugx(2, "%s: matching '%s' against '%s'", __func__, target_host_port, NUT_STRARG(tmp->section)); - if (tmp->section && !strcmp(tmp->section, target_host_port)) { - return tmp; + /* 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]) { + target_host_port++; + upsdebugx(2, "%s: retry with shorter '@host:port' for host defaults (without the user part)", __func__); + + tmp = authconf_list; + while (tmp) { + upsdebugx(2, "%s: matching '%s' against '%s'", __func__, target_host_port, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, target_host_port)) { + retval = tmp; + goto finished; + } + tmp = tmp->next; + } } - tmp = tmp->next; } - } - /* 3. Global defaults (section == NULL) */ - return global_defaults; + /* 3. Global defaults (section == NULL) */ + retval = global_defaults; + +finished: + free(sect_user); + free(sect_host); + free(sect_port); + free(normalized_sect_name); + return retval; + } } From 11e7345678599084a34798102d28a92346a9e8b1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 00:11:50 +0200 Subject: [PATCH 018/108] tests/test_authconf.c: add a Non-exact match test case [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 3d5f764d27..80220cc614 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -140,7 +140,30 @@ int main(int argc, char **argv) return 1; } - /* 4. Include match */ + /* 4. Non-exact match */ + printf("Checking non-exact match for 'somebody@localhost:12345'\n"); + ac = upscli_find_authconf("somebody", "localhost", "12345"); + 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 + && ac->forcessl == 1 + ) { + printf("Non-exact match OK\n"); + } else { + printf("Non-exact match FAILED (wrong values): expecting user='%s' pass=\n", "somebody"); + return 1; + } + } else { + printf("Non-exact match FAILED (no ac)\n"); + return 1; + } + + /* 5. Include match */ printf("Checking include match for '@otherhost'\n"); ac = upscli_find_authconf(NULL, "otherhost", NULL); snprintf(buf, sizeof(buf), "@otherhost:%u", (unsigned int)NUT_PORT); @@ -160,7 +183,7 @@ int main(int argc, char **argv) return 1; } - /* 5. No bogus hits */ + /* 6. No bogus hits */ printf("Checking NO match for '@otherhost:portnum' other than global section\n"); ac = upscli_find_authconf(NULL, "otherhost", "portnum"); if (ac) { From 4b5b2091db94ce64e1edd797f47968505266957e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 10:25:01 +0200 Subject: [PATCH 019/108] clients/authconf.c: fix static analysis warnings [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index bb6598d3d0..bce9ec150b 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -22,6 +22,9 @@ #ifndef WIN32 # include +# include +# include +# include #else /* => WIN32 */ /* Those 2 files for support of getaddrinfo, getnameinfo and freeaddrinfo on Windows 2000 and older versions */ @@ -353,7 +356,7 @@ int upscli_normalize_auth_section_parts( if (se) { char portbuf[16]; - if (snprintf(portbuf, sizeof(portbuf), "%u", ntohs(se->s_port) < 1)) { + 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; } @@ -366,7 +369,7 @@ int upscli_normalize_auth_section_parts( } } else { char portbuf[16]; - if (snprintf(portbuf, sizeof(portbuf), "%u", NUT_PORT) < 1) { + if (snprintf(portbuf, sizeof(portbuf), "%u", (unsigned int)NUT_PORT) < 1) { upsdebugx(1, "%s: Failed to construct default port number", __func__); goto failed; } From 6fd19678ad0ae24ea405be3b25099babca2a8a93 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 10:27:15 +0200 Subject: [PATCH 020/108] clients/authconf.{c,h}: refactor static upscli_add_authconf() with a public upscli_create_authconf() [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 38 ++++++++++++++++++++++++++------------ clients/authconf.h | 5 ++++- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index bce9ec150b..f0a94f1f1e 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -48,9 +48,9 @@ upscli_authconf_t *upscli_get_authconf_list(void) return authconf_list; } -static upscli_authconf_t *upscli_add_authconf(const char *section) +upscli_authconf_t *upscli_create_authconf(const char *section) { - upscli_authconf_t *node = xcalloc(1, sizeof(upscli_authconf_t)); + upscli_authconf_t *node = (upscli_authconf_t *)xcalloc(1, sizeof(upscli_authconf_t)); if (section) { node->section = xstrdup(section); @@ -58,6 +58,13 @@ static upscli_authconf_t *upscli_add_authconf(const char *section) node->certverify = -1; node->forcessl = -1; + return node; +} + +static upscli_authconf_t *upscli_add_authconf(const char *section) +{ + upscli_authconf_t *node = upscli_create_authconf(section); + /* Append to list */ if (!authconf_list) { authconf_list = node; @@ -749,6 +756,8 @@ int upscli_read_authconf(const char *filename, int fatal_errors) upscli_authconf_t *upscli_find_authconf(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 (!host && !port && !user) { /* Global section only */ /* Should we just return global_defaults? */ @@ -769,19 +778,23 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons upscli_authconf_t *retval = global_defaults, *tmp = NULL; if (upscli_normalize_auth_section_parts( - &normalized_sect_name, - §_user, - &fixed_sect_user, - §_host, - §_port) < 0) - goto finished; /* return default */ + &normalized_sect_name, + §_user, + &fixed_sect_user, + §_host, + §_port) < 0 + ) { + upsdebugx(2, "%s: returning global defaults: could not upscli_normalize_auth_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(2, "%s: matching '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + 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; @@ -792,14 +805,14 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons const char *target_host_port = strchr(normalized_sect_name, '@'); if (target_host_port[1]) { - target_host_port++; - upsdebugx(2, "%s: retry with shorter '@host:port' for host defaults (without the user part)", __func__); + upsdebugx(4, "%s: retry with shorter '@host:port' for host defaults (without the user part)", __func__); tmp = authconf_list; while (tmp) { - upsdebugx(2, "%s: matching '%s' against '%s'", __func__, target_host_port, NUT_STRARG(tmp->section)); + 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; @@ -808,6 +821,7 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons } /* 3. Global defaults (section == NULL) */ + upsdebugx(2, "%s: returning global defaults: no more specific hit was found", __func__); retval = global_defaults; finished: diff --git a/clients/authconf.h b/clients/authconf.h index 7f7d802e4f..e3b642e218 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -35,9 +35,12 @@ typedef struct upscli_authconf_s { struct upscli_authconf_s *next; } upscli_authconf_t; -/** Get the list of all parsed authentication configurations */ +/** 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() it manually */ +upscli_authconf_t *upscli_create_authconf(const char *section); + /** Free an authentication configuration item (if not NULL) and return its "next" pointer */ upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); From 3dfcbfc8fe3521d9b7042fb3c14a37db9f2dc1c3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 16:04:18 +0200 Subject: [PATCH 021/108] clients/authconf.c, docs/man/nutauth.conf.txt: revise section title parsing, INCLUDE handling, naming of [_global_defaults], code comments etc. [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 65 ++++++++++++++++++++++++++++++++------- docs/man/nutauth.conf.txt | 48 ++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index f0a94f1f1e..9f2b6bf17b 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -37,9 +37,20 @@ #endif /* WIN32 */ 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); @@ -465,7 +476,7 @@ int upscli_split_auth_section(const char *sect_name, 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) { @@ -474,15 +485,18 @@ int upscli_split_auth_section(const char *sect_name, if (colon) { sect_host[colon - at - 1] = '\0'; } - } /* else keep NULL */ + } /* 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 NULL */ + } /* else keep sect_host=NULL */ } if (colon && colon[1]) { @@ -542,17 +556,26 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) const char *end_bracket = NULL; upscli_authconf_t *tmp = NULL; - if (!global_scope) { - upslogx(LOG_WARNING, "Section header ignored in included file with section-scope"); - return; - } + current_section_ignored = 0; sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ end_bracket = strchr(sect_name, ']'); - if (!end_bracket) { + if (!end_bracket || !strcmp(sect_name, "_global_defaults")) { free(sect_name); - fatalx(EXIT_FAILURE, "%s: Invalid section header format: %s", __func__, arg[0]); + + if (global_scope) { + /* Subsequent lines will (re-)populate global_defaults */ + current_section = NULL; + return; + } + + current_section_ignored = 1; + upslogx(LOG_WARNING, "%s: Invalid nutauth section header format " + "in a non-global context, section contents will be ignored: %s", + __func__, arg[0]); + return; } + *(char *)(end_bracket) = '\0'; /* forget trailing ']' and any characters after it (comments etc.) */ if (upscli_split_auth_section(sect_name, &normalized_sect_name, @@ -567,9 +590,21 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) 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 */ - tmp = authconf_list; + 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; @@ -608,6 +643,10 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) return; } + if (current_section_ignored) { + return; + } + /* INCLUDE support */ if (!strcasecmp(arg[0], "INCLUDE_REQUIRED")) { if (numargs < 2) { @@ -647,7 +686,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) if (current_section) { set_authconf_val(current_section, var, val); } else { - /* Modifying global defaults */ + /* Creating/modifying global defaults */ if (!global_defaults) { global_defaults = upscli_add_authconf(NULL); } @@ -693,6 +732,10 @@ static int parse_authconf_file(const char *filename, int fatal_errors, int globa 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; } diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt index fd0afe191d..0c02083cb6 100644 --- a/docs/man/nutauth.conf.txt +++ b/docs/man/nutauth.conf.txt @@ -21,6 +21,9 @@ 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: @@ -33,18 +36,49 @@ server being contacted. Supported section formats are: *[user@host:port]*:: Defines credentials and settings for a specific user on a specific - host and port. + 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). -An empty 'host' component value is interpreted as `localhost`, e.g. in `[@: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 name `[default]` is reserved and should not be used as a section -header; use the global scope (before any section header) for default settings. +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 @@ -129,7 +163,11 @@ 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. +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: From 695d7d3d362b698f904e8499572797a81ff3537a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 16:42:15 +0200 Subject: [PATCH 022/108] docs/man/*auth*.txt, docs/man/Makefile.am: reshuffle nutauth.conf related methods vs population of man pages [#3329] Signed-off-by: Jim Klimov --- docs/man/Makefile.am | 50 +++++++------- docs/man/upscli_create_authconf.txt | 65 +++++++++++++++++++ ...conf_list.txt => upscli_dump_authconf.txt} | 23 +++---- docs/man/upscli_find_authconf.txt | 12 +--- docs/man/upscli_free_authconf_list.txt | 34 ++++++++++ docs/man/upscli_get_authconf_list.txt | 31 ++------- 6 files changed, 145 insertions(+), 70 deletions(-) create mode 100644 docs/man/upscli_create_authconf.txt rename docs/man/{upscli_dump_authconf_list.txt => upscli_dump_authconf.txt} (83%) create mode 100644 docs/man/upscli_free_authconf_list.txt diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 64d0d527e0..e9fc2c28b8 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -556,10 +556,12 @@ SRC_DEV_PAGES = \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_upslog_set_debug_level.txt \ + upscli_create_authconf.txt \ + upscli_dump_authconf_list.txt \ + upscli_find_authconf.txt \ + upscli_free_authconf_list.txt \ upscli_get_authconf_list.txt \ upscli_read_authconf.txt \ - upscli_find_authconf.txt \ - upscli_dump_authconf_list.txt \ upscli_str_add_unique_token.txt \ upscli_str_contains_token.txt \ libnutclient.txt \ @@ -756,15 +758,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_get_authconf_list.$(MAN_SECTION_API) \ - upscli_normalize_auth_section_parts.$(MAN_SECTION_API) \ - upscli_split_auth_section.$(MAN_SECTION_API) \ + upscli_create_authconf.$(MAN_SECTION_API) \ + $(UPSCLI_CREATE_AUTHCONF_DEPS) \ + upscli_dump_authconf.$(MAN_SECTION_API) \ + $(UPSCLI_DUMP_AUTHCONF_DEPS) \ + upscli_find_authconf.$(MAN_SECTION_API) \ + $(UPSCLI_FIND_AUTHCONF_DEPS) \ upscli_free_authconf_list.$(MAN_SECTION_API) \ + upscli_get_authconf_list.$(MAN_SECTION_API) \ upscli_read_authconf.$(MAN_SECTION_API) \ - upscli_find_authconf.$(MAN_SECTION_API) \ - upscli_free_authconf.$(MAN_SECTION_API) \ - upscli_dump_authconf_list.$(MAN_SECTION_API) \ - upscli_dump_authconf.$(MAN_SECTION_API) \ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS) \ upscli_str_add_unique_token.$(MAN_SECTION_API) \ upscli_str_contains_token.$(MAN_SECTION_API) \ @@ -832,19 +834,17 @@ upscli_sendline_timeout_may_disconnect.$(MAN_SECTION_API): upscli_sendline.$(MAN upscli_tryconnect.$(MAN_SECTION_API): upscli_connect.$(MAN_SECTION_API) touch $@ -upscli_normalize_auth_section_parts.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) - touch $@ +UPSCLI_CREATE_AUTHCONF_DEPS = upscli_free_authconf.$(MAN_SECTION_API) +UPSCLI_DUMP_AUTHCONF_DEPS = upscli_dump_authconf_list.$(MAN_SECTION_API) +UPSCLI_FIND_AUTHCONF_DEPS = upscli_normalize_auth_section_parts.$(MAN_SECTION_API) upscli_split_auth_section.$(MAN_SECTION_API) -upscli_split_auth_section.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) +$(UPSCLI_CREATE_AUTHCONF_DEPS): upscli_create_authconf.$(MAN_SECTION_API) touch $@ -upscli_free_authconf_list.$(MAN_SECTION_API): upscli_get_authconf_list.$(MAN_SECTION_API) +$(UPSCLI_DUMP_AUTHCONF_DEPS): upscli_dump_authconf.$(MAN_SECTION_API) touch $@ -upscli_free_authconf.$(MAN_SECTION_API): upscli_find_authconf.$(MAN_SECTION_API) - touch $@ - -upscli_dump_authconf.$(MAN_SECTION_API): upscli_dump_authconf_list.$(MAN_SECTION_API) +$(UPSCLI_FIND_AUTHCONF_DEPS): upscli_find_authconf.$(MAN_SECTION_API) touch $@ nutscan_scan_ip_range_snmp.$(MAN_SECTION_API): nutscan_scan_snmp.$(MAN_SECTION_API) @@ -921,10 +921,12 @@ INST_HTML_DEV_MANS = \ upscli_strerror.html \ upscli_upserror.html \ upscli_upslog_set_debug_level.html \ + upscli_create_authconf.html \ + upscli_dump_authconf.html \ + upscli_find_authconf.html \ + upscli_free_authconf_list.html \ upscli_get_authconf_list.html \ upscli_read_authconf.html \ - upscli_find_authconf.html \ - upscli_dump_authconf_list.html \ upscli_str_add_unique_token.html \ upscli_str_contains_token.html \ libnutclient.html \ @@ -992,19 +994,19 @@ 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_normalize_auth_section_parts.html: upscli_get_authconf_list.html +upscli_normalize_auth_section_parts.html: upscli_find_authconf.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_split_auth_section.html: upscli_get_authconf_list.html +upscli_split_auth_section.html: upscli_find_authconf.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_free_authconf_list.html: upscli_get_authconf_list.html +upscli_free_authconf_list.html: upscli_free_authconf.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_free_authconf.html: upscli_find_authconf.html +upscli_free_authconf.html: upscli_create_authconf.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_dump_authconf.html: upscli_dump_authconf_list.html +upscli_dump_authconf_list.html: upscli_dump_authconf.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ nutscan_scan_ip_range_snmp.html: nutscan_scan_snmp.html diff --git a/docs/man/upscli_create_authconf.txt b/docs/man/upscli_create_authconf.txt new file mode 100644 index 0000000000..1cdbc72fef --- /dev/null +++ b/docs/man/upscli_create_authconf.txt @@ -0,0 +1,65 @@ +UPSCLI_CREATE_AUTHCONF(3) +========================= + +NAME +---- + +upscli_create_authconf, upscli_free_authconf - Create or free an individual +authentication configuration item which is not necessarily in the 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 */ + 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(const char *section); + + upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); +------ + +DESCRIPTION +----------- + +The *upscli_create_authconf()* 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_free_authconf()* 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()* function returns a pointer to the new structure, +or `NULL` if an error occurred. + +The *upscli_free_authconf()* 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[3], +linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[3], +linkman:upscli_get_authconf_list[3], linkman:upscli_dump_authconf_list[3] diff --git a/docs/man/upscli_dump_authconf_list.txt b/docs/man/upscli_dump_authconf.txt similarity index 83% rename from docs/man/upscli_dump_authconf_list.txt rename to docs/man/upscli_dump_authconf.txt index 3eefae6833..224e93eac2 100644 --- a/docs/man/upscli_dump_authconf_list.txt +++ b/docs/man/upscli_dump_authconf.txt @@ -1,10 +1,10 @@ -UPSCLI_DUMP_AUTHCONF_LIST(3) +UPSCLI_DUMP_AUTHCONF(3) ============================ NAME ---- -upscli_dump_authconf_list, upscli_dump_authconf - Print authentication configuration list +upscli_dump_authconf, upscli_dump_authconf_list - Print authentication configuration node or list SYNOPSIS -------- @@ -12,20 +12,19 @@ SYNOPSIS ------ #include - size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug); - int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node, int for_debug); + + size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug); ------ DESCRIPTION ----------- -The *upscli_dump_authconf_list()* function prints the entire internal list of -authentication configurations to the specified 'stream'. If 'stream' is NULL, -it defaults to `stdout`. - The *upscli_dump_authconf()* function prints a single configuration 'node' -to the specified 'stream'. +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. @@ -48,12 +47,14 @@ characters). RETURN VALUE ------------ -The *upscli_dump_authconf_list()* function returns the number of nodes printed. - The *upscli_dump_authconf()* 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[3], linkman:upscli_read_authconf[3], linkman:upscli_get_authconf_list[3] diff --git a/docs/man/upscli_find_authconf.txt b/docs/man/upscli_find_authconf.txt index bb84130524..c4d16deeef 100644 --- a/docs/man/upscli_find_authconf.txt +++ b/docs/man/upscli_find_authconf.txt @@ -5,8 +5,9 @@ NAME ---- upscli_find_authconf, upscli_normalize_auth_section_parts, -upscli_split_auth_section, upscli_free_authconf - Find, -parse name, and free authentication configuration items +upscli_split_auth_section - Find authentication configuration +items by their name components; help parse and normalize +name components for authentication configuration items SYNOPSIS -------- @@ -32,8 +33,6 @@ SYNOPSIS int *out_fixed_sect_user, char **normalized_sect_host, char **normalized_sect_port); - - upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); ------ DESCRIPTION @@ -68,11 +67,6 @@ 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. -The *upscli_free_authconf()* 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 ------------ diff --git a/docs/man/upscli_free_authconf_list.txt b/docs/man/upscli_free_authconf_list.txt new file mode 100644 index 0000000000..bc16382354 --- /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[3], linkman:upscli_find_authconf[3], +linkman:upscli_get_authconf_list[3], linkman:upscli_dump_authconf_list[3], +linkman:upscli_create_authconf[3], linkman:upscli_free_authconf[3] diff --git a/docs/man/upscli_get_authconf_list.txt b/docs/man/upscli_get_authconf_list.txt index 9294ce64c2..0db3ef57f1 100644 --- a/docs/man/upscli_get_authconf_list.txt +++ b/docs/man/upscli_get_authconf_list.txt @@ -4,7 +4,7 @@ UPSCLI_GET_AUTHCONF_LIST(3) NAME ---- -upscli_get_authconf_list, upscli_free_authconf_list - Get and free the list of authentication configurations +upscli_get_authconf_list - Get the list of known authentication configurations SYNOPSIS -------- @@ -13,8 +13,6 @@ SYNOPSIS #include upscli_authconf_t *upscli_get_authconf_list(void); - - void upscli_free_authconf_list(void); ------ DESCRIPTION @@ -24,28 +22,8 @@ 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[3]. -Each element in the list is of type `upscli_authconf_t`: - -[source,c] ----------- -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 */ - int certverify; /* -1 = unset, 0 = off, 1 = on */ - int forcessl; /* -1 = unset, 0 = off, 1 = on */ - - struct upscli_authconf_s *next; -} upscli_authconf_t; ----------- - -The *upscli_free_authconf_list()* function frees the memory allocated for the -entire list and resets the internal list pointer to NULL. +Each element in the list is of type `upscli_authconf_t` as detailed in +linkman:upscli_create_authconf[3]. RETURN VALUE ------------ @@ -57,5 +35,6 @@ initialized by linkman:upscli_read_authconf[3]. SEE ALSO -------- +linkman:upscli_create_authconf[3], linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[3], -linkman:upscli_free_authconf[3], linkman:upscli_dump_authconf_list[3] +linkman:upscli_free_authconf_list[3], linkman:upscli_dump_authconf_list[3] From 7bd2e30c909527956f6882b39ded3466cb785ef1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 16:42:43 +0200 Subject: [PATCH 023/108] clients/authconf.c: upscli_create_authconf(): handle failed calloc() by returning NULL; fail fatally in upscli_add_authconf() if needed [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/clients/authconf.c b/clients/authconf.c index 9f2b6bf17b..9368fba158 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -61,7 +61,10 @@ upscli_authconf_t *upscli_get_authconf_list(void) upscli_authconf_t *upscli_create_authconf(const char *section) { - upscli_authconf_t *node = (upscli_authconf_t *)xcalloc(1, sizeof(upscli_authconf_t)); + upscli_authconf_t *node = (upscli_authconf_t *)calloc(1, sizeof(upscli_authconf_t)); + + if (!node) + return NULL; if (section) { node->section = xstrdup(section); @@ -76,6 +79,10 @@ static upscli_authconf_t *upscli_add_authconf(const char *section) { upscli_authconf_t *node = upscli_create_authconf(section); + if (!node) { + fatalx(EXIT_FAILURE, "Failed to create nutauth configuration node for section '%s'", section); + } + /* Append to list */ if (!authconf_list) { authconf_list = node; From 9e2b29d918ba79ff092c485a8150d747fa5ffeae Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 16:58:59 +0200 Subject: [PATCH 024/108] clients/authconf.{c,h}, tests/test_authconf.c, docs/man/*auth*, docs/man/Makefile.am: rename file/list/item/section methods consistently with their purpose [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 34 +++++++------- clients/authconf.h | 16 +++---- docs/man/Makefile.am | 44 +++++++++---------- docs/man/nutauth.conf.txt | 4 +- ...nf.txt => upscli_create_authconf_item.txt} | 22 +++++----- ...conf.txt => upscli_dump_authconf_item.txt} | 14 +++--- ...conf.txt => upscli_find_authconf_item.txt} | 30 ++++++------- docs/man/upscli_free_authconf_list.txt | 4 +- docs/man/upscli_get_authconf_list.txt | 8 ++-- docs/man/upscli_init.txt | 2 +- ...conf.txt => upscli_read_authconf_file.txt} | 14 +++--- tests/test_authconf.c | 18 ++++---- 12 files changed, 105 insertions(+), 105 deletions(-) rename docs/man/{upscli_create_authconf.txt => upscli_create_authconf_item.txt} (64%) rename docs/man/{upscli_dump_authconf.txt => upscli_dump_authconf_item.txt} (76%) rename docs/man/{upscli_find_authconf.txt => upscli_find_authconf_item.txt} (68%) rename docs/man/{upscli_read_authconf.txt => upscli_read_authconf_file.txt} (81%) diff --git a/clients/authconf.c b/clients/authconf.c index 9368fba158..270713caf7 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -59,7 +59,7 @@ upscli_authconf_t *upscli_get_authconf_list(void) return authconf_list; } -upscli_authconf_t *upscli_create_authconf(const char *section) +upscli_authconf_t *upscli_create_authconf_item(const char *section) { upscli_authconf_t *node = (upscli_authconf_t *)calloc(1, sizeof(upscli_authconf_t)); @@ -77,7 +77,7 @@ upscli_authconf_t *upscli_create_authconf(const char *section) static upscli_authconf_t *upscli_add_authconf(const char *section) { - upscli_authconf_t *node = upscli_create_authconf(section); + upscli_authconf_t *node = upscli_create_authconf_item(section); if (!node) { fatalx(EXIT_FAILURE, "Failed to create nutauth configuration node for section '%s'", section); @@ -97,7 +97,7 @@ static upscli_authconf_t *upscli_add_authconf(const char *section) return node; } -upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node) +upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node) { if (node) { upscli_authconf_t *next = node->next; @@ -121,7 +121,7 @@ upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node) static int upscli_dump_authconf_line_str(FILE *restrict stream, const char *var, const char *val, const char *indent, int for_debug) { - /* Assume sane inputs from upscli_dump_authconf(); val may be NULL */ + /* Assume sane inputs from upscli_dump_authconf_item(); val may be NULL */ int res = 0; if (!val) { if (for_debug) { @@ -154,7 +154,7 @@ static int upscli_dump_authconf_line_str(FILE *restrict stream, const char *var, static int upscli_dump_authconf_line_int(FILE *restrict stream, const char *var, int val, const char *indent, int for_debug) { - /* Assume sane inputs from upscli_dump_authconf(); val may be NULL */ + /* Assume sane inputs from upscli_dump_authconf_item(); val may be NULL */ int res; /* TOTHINK: Print "-1" values when not running "for_debug"? @@ -172,7 +172,7 @@ static int upscli_dump_authconf_line_int(FILE *restrict stream, const char *var, return res; } -int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node, int for_debug) +int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug) { char *indent = NULL; int res = 0, ret = 0; @@ -256,7 +256,7 @@ size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug) while (node) { count++; - upscli_dump_authconf(stream, node, for_debug); + upscli_dump_authconf_item(stream, node, for_debug); node = node->next; } @@ -268,7 +268,7 @@ void upscli_free_authconf_list(void) upscli_authconf_t *node = authconf_list; while (node) { - node = upscli_free_authconf(node); + node = upscli_free_authconf_item(node); } authconf_list = NULL; @@ -332,7 +332,7 @@ static void authconf_err(const char *errmsg) upslogx(LOG_ERR, "Error in parseconf(authconf): %s", errmsg); } -int upscli_normalize_auth_section_parts( +int upscli_normalize_authconf_section_parts( char **out_normalized_sect_name, char **p_sect_user, int *out_fixed_sect_user, @@ -348,7 +348,7 @@ int upscli_normalize_auth_section_parts( * those data points returned. */ if (!p_sect_user || !p_sect_host || !p_sect_port) { - upslogx(LOG_ERR, "upscli_normalize_auth_section_parts: NULL pointer-to-string argument provided"); + upslogx(LOG_ERR, "upscli_normalize_authconf_section_parts: NULL pointer-to-string argument provided"); return -1; } @@ -444,7 +444,7 @@ int upscli_normalize_auth_section_parts( return -1; } -int upscli_split_auth_section(const char *sect_name, +int upscli_split_authconf_section(const char *sect_name, char **normalized_sect_name, char **normalized_sect_user, int *out_fixed_sect_user, @@ -512,7 +512,7 @@ int upscli_split_auth_section(const char *sect_name, if (!sect_port) goto failed; } - if (upscli_normalize_auth_section_parts( + if (upscli_normalize_authconf_section_parts( normalized_sect_name, §_user, &fixed_sect_user, §_host, §_port) < 0 @@ -585,7 +585,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) *(char *)(end_bracket) = '\0'; /* forget trailing ']' and any characters after it (comments etc.) */ - if (upscli_split_auth_section(sect_name, &normalized_sect_name, + if (upscli_split_authconf_section(sect_name, &normalized_sect_name, §_user, ¤t_section_with_fixed_username, §_host, §_port) < 0 ) { @@ -747,7 +747,7 @@ static int parse_authconf_file(const char *filename, int fatal_errors, int globa return 1; } -int upscli_read_authconf(const char *filename, int fatal_errors) +int upscli_read_authconf_file(const char *filename, int fatal_errors) { char fn[NUT_PATH_MAX + 1]; @@ -804,7 +804,7 @@ int upscli_read_authconf(const char *filename, int fatal_errors) return parse_authconf_file(filename, fatal_errors, 1); } -upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port) +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)); @@ -827,14 +827,14 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons int fixed_sect_user = 0; upscli_authconf_t *retval = global_defaults, *tmp = NULL; - if (upscli_normalize_auth_section_parts( + 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_auth_section_parts()", __func__); + upsdebugx(2, "%s: returning global defaults: could not upscli_normalize_authconf_section_parts()", __func__); goto finished; /* return default */ } diff --git a/clients/authconf.h b/clients/authconf.h index e3b642e218..a703db0347 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -38,11 +38,11 @@ typedef struct upscli_authconf_s { /** 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() it manually */ -upscli_authconf_t *upscli_create_authconf(const char *section); +/** Create a one-off configuration item, upscli_free_authconf_item() it manually */ +upscli_authconf_t *upscli_create_authconf_item(const char *section); /** Free an authentication configuration item (if not NULL) and return its "next" pointer */ -upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); +upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node); /** Free the list of authentication configurations */ void upscli_free_authconf_list(void); @@ -53,7 +53,7 @@ void upscli_free_authconf_list(void); * (whichever is found first); then one can follow `INCLUDE` trail if needed. * Returns -1 on error, 1 on success */ -int upscli_read_authconf(const char *filename, int fatal_errors); +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 @@ -61,7 +61,7 @@ int upscli_read_authconf(const char *filename, int fatal_errors); * The out_* values are optional and may be NULL if you do not want * those data points returned. */ -int upscli_normalize_auth_section_parts( +int upscli_normalize_authconf_section_parts( char **out_normalized_sect_name, char **p_sect_user, int *out_fixed_sect_user, @@ -73,7 +73,7 @@ int upscli_normalize_auth_section_parts( * 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_auth_section(const char *sect_name, +int upscli_split_authconf_section(const char *sect_name, char **normalized_sect_name, char **normalized_sect_user, int *out_fixed_sect_user, @@ -83,7 +83,7 @@ int upscli_split_auth_section(const char *sect_name, /** Find the best matching authconf for a given connection string; * if all args are NULL, return the global section or NULL if none such in the list. */ -upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, const char *port); +upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, const char *port); /** Print one node to the specified stream (stdout if NULL), * return code similar to fprintf() - sum of printed characters. @@ -94,7 +94,7 @@ upscli_authconf_t *upscli_find_authconf(const char *user, const char *host, cons * 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(FILE *restrict stream, upscli_authconf_t *node, int for_debug); +int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug); /** Print ultimate configuration to the specified stream (stdout if NULL) * and return the number of nodes in the current authconf list */ diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index e9fc2c28b8..737701deb8 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -556,12 +556,12 @@ SRC_DEV_PAGES = \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_upslog_set_debug_level.txt \ - upscli_create_authconf.txt \ - upscli_dump_authconf_list.txt \ - upscli_find_authconf.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.txt \ + upscli_read_authconf_file.txt \ upscli_str_add_unique_token.txt \ upscli_str_contains_token.txt \ libnutclient.txt \ @@ -758,15 +758,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.$(MAN_SECTION_API) \ + upscli_create_authconf_item.$(MAN_SECTION_API) \ $(UPSCLI_CREATE_AUTHCONF_DEPS) \ - upscli_dump_authconf.$(MAN_SECTION_API) \ + upscli_dump_authconf_item.$(MAN_SECTION_API) \ $(UPSCLI_DUMP_AUTHCONF_DEPS) \ - upscli_find_authconf.$(MAN_SECTION_API) \ + 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.$(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) \ @@ -834,17 +834,17 @@ 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_free_authconf.$(MAN_SECTION_API) +UPSCLI_CREATE_AUTHCONF_DEPS = upscli_free_authconf_item.$(MAN_SECTION_API) UPSCLI_DUMP_AUTHCONF_DEPS = upscli_dump_authconf_list.$(MAN_SECTION_API) -UPSCLI_FIND_AUTHCONF_DEPS = upscli_normalize_auth_section_parts.$(MAN_SECTION_API) upscli_split_auth_section.$(MAN_SECTION_API) +UPSCLI_FIND_AUTHCONF_DEPS = upscli_normalize_authconf_section_parts.$(MAN_SECTION_API) upscli_split_authconf_section.$(MAN_SECTION_API) -$(UPSCLI_CREATE_AUTHCONF_DEPS): upscli_create_authconf.$(MAN_SECTION_API) +$(UPSCLI_CREATE_AUTHCONF_DEPS): upscli_create_authconf_item.$(MAN_SECTION_API) touch $@ -$(UPSCLI_DUMP_AUTHCONF_DEPS): upscli_dump_authconf.$(MAN_SECTION_API) +$(UPSCLI_DUMP_AUTHCONF_DEPS): upscli_dump_authconf_item.$(MAN_SECTION_API) touch $@ -$(UPSCLI_FIND_AUTHCONF_DEPS): upscli_find_authconf.$(MAN_SECTION_API) +$(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) @@ -921,12 +921,12 @@ INST_HTML_DEV_MANS = \ upscli_strerror.html \ upscli_upserror.html \ upscli_upslog_set_debug_level.html \ - upscli_create_authconf.html \ - upscli_dump_authconf.html \ - upscli_find_authconf.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.html \ + upscli_read_authconf_file.html \ upscli_str_add_unique_token.html \ upscli_str_contains_token.html \ libnutclient.html \ @@ -994,19 +994,19 @@ 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_normalize_auth_section_parts.html: upscli_find_authconf.html +upscli_normalize_authconf_section_parts.html: upscli_find_authconf_item.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_split_auth_section.html: upscli_find_authconf.html +upscli_split_authconf_section.html: upscli_find_authconf_item.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_free_authconf_list.html: upscli_free_authconf.html +upscli_free_authconf_list.html: upscli_free_authconf_item.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_free_authconf.html: upscli_create_authconf.html +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.html +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 diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt index 0c02083cb6..95df04c1a1 100644 --- a/docs/man/nutauth.conf.txt +++ b/docs/man/nutauth.conf.txt @@ -10,7 +10,7 @@ DESCRIPTION ----------- This file is read by the NUT client library linkman:libupsclient[3] via -linkman:upscli_read_authconf[3]. It allows users to define default and +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. @@ -179,7 +179,7 @@ Example: SEE ALSO -------- -linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[3], +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] diff --git a/docs/man/upscli_create_authconf.txt b/docs/man/upscli_create_authconf_item.txt similarity index 64% rename from docs/man/upscli_create_authconf.txt rename to docs/man/upscli_create_authconf_item.txt index 1cdbc72fef..276f3ee2dc 100644 --- a/docs/man/upscli_create_authconf.txt +++ b/docs/man/upscli_create_authconf_item.txt @@ -1,10 +1,10 @@ -UPSCLI_CREATE_AUTHCONF(3) -========================= +UPSCLI_CREATE_AUTHCONF_ITEM(3) +============================== NAME ---- -upscli_create_authconf, upscli_free_authconf - Create or free an individual +upscli_create_authconf_item, upscli_free_authconf_item - Create or free an individual authentication configuration item which is not necessarily in the list SYNOPSIS @@ -28,21 +28,21 @@ SYNOPSIS struct upscli_authconf_s *next; } upscli_authconf_t; - upscli_authconf_t *upscli_create_authconf(const char *section); + upscli_authconf_t *upscli_create_authconf_item(const char *section); - upscli_authconf_t *upscli_free_authconf(upscli_authconf_t *node); + upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node); ------ DESCRIPTION ----------- -The *upscli_create_authconf()* function allocates the memory for an +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_free_authconf()* function frees the memory allocated for +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 @@ -51,15 +51,15 @@ internal list. RETURN VALUE ------------ -The *upscli_create_authconf()* function returns a pointer to the new structure, +The *upscli_create_authconf_item()* function returns a pointer to the new structure, or `NULL` if an error occurred. -The *upscli_free_authconf()* function returns a pointer to the next element +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[3], -linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[3], +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.txt b/docs/man/upscli_dump_authconf_item.txt similarity index 76% rename from docs/man/upscli_dump_authconf.txt rename to docs/man/upscli_dump_authconf_item.txt index 224e93eac2..77f452582f 100644 --- a/docs/man/upscli_dump_authconf.txt +++ b/docs/man/upscli_dump_authconf_item.txt @@ -1,10 +1,10 @@ -UPSCLI_DUMP_AUTHCONF(3) +UPSCLI_DUMP_AUTHCONF_ITEM(3) ============================ NAME ---- -upscli_dump_authconf, upscli_dump_authconf_list - Print authentication configuration node or list +upscli_dump_authconf_item, upscli_dump_authconf_list - Print authentication configuration node or list SYNOPSIS -------- @@ -12,7 +12,7 @@ SYNOPSIS ------ #include - int upscli_dump_authconf(FILE *restrict stream, upscli_authconf_t *node, int for_debug); + int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug); size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug); ------ @@ -20,7 +20,7 @@ SYNOPSIS DESCRIPTION ----------- -The *upscli_dump_authconf()* function prints a single configuration 'node' +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 @@ -47,7 +47,7 @@ characters). RETURN VALUE ------------ -The *upscli_dump_authconf()* function returns the return value of the +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 @@ -56,5 +56,5 @@ seen in the list (and likely printed). SEE ALSO -------- -linkman:upscli_create_authconf[3], -linkman:upscli_read_authconf[3], linkman:upscli_get_authconf_list[3] +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.txt b/docs/man/upscli_find_authconf_item.txt similarity index 68% rename from docs/man/upscli_find_authconf.txt rename to docs/man/upscli_find_authconf_item.txt index c4d16deeef..d51753b444 100644 --- a/docs/man/upscli_find_authconf.txt +++ b/docs/man/upscli_find_authconf_item.txt @@ -1,11 +1,11 @@ -UPSCLI_FIND_AUTHCONF(3) -======================= +UPSCLI_FIND_AUTHCONF_ITEM(3) +============================ NAME ---- -upscli_find_authconf, upscli_normalize_auth_section_parts, -upscli_split_auth_section - Find authentication configuration +upscli_find_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 @@ -15,19 +15,19 @@ SYNOPSIS ------ #include - upscli_authconf_t *upscli_find_authconf( + upscli_authconf_t *upscli_find_authconf_item( const char *user, const char *host, const char *port); - int upscli_normalize_auth_section_parts( + 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_auth_section(const char *sect_name, + int upscli_split_authconf_section(const char *sect_name, char **normalized_sect_name, char **normalized_sect_user, int *out_fixed_sect_user, @@ -38,7 +38,7 @@ SYNOPSIS DESCRIPTION ----------- -The *upscli_find_authconf()* function searches the internal list of +The *upscli_find_authconf_item()* function searches the internal list of authentication configurations for the best match for the given 'user', 'host', and 'port'. @@ -51,16 +51,16 @@ The matching logic follows this priority: If a specific match is found, any missing fields in that section are inherited from the global defaults. -The "*upscli_split_auth_section()*" function splits a `sect_name` which may be +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_auth_section_parts()*" normalizes the values (e.g. a +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_auth_section()*", must be not `NULL` in case of -"*upscli_normalize_auth_section_parts()*"). +"*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]` @@ -70,17 +70,17 @@ searching in the list. RETURN VALUE ------------ -The *upscli_find_authconf()* function returns a pointer to a `upscli_authconf_t` +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()* function returns the last known value of the `next` +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[3], linkman:upscli_get_authconf_list[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_get_authconf_list[3], linkman:upscli_free_authconf_list[3] diff --git a/docs/man/upscli_free_authconf_list.txt b/docs/man/upscli_free_authconf_list.txt index bc16382354..67525afa79 100644 --- a/docs/man/upscli_free_authconf_list.txt +++ b/docs/man/upscli_free_authconf_list.txt @@ -29,6 +29,6 @@ The *upscli_free_authconf_list()* function returns nothing. SEE ALSO -------- -linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[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], -linkman:upscli_create_authconf[3], linkman:upscli_free_authconf[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 index 0db3ef57f1..bf09ed4ea8 100644 --- a/docs/man/upscli_get_authconf_list.txt +++ b/docs/man/upscli_get_authconf_list.txt @@ -20,7 +20,7 @@ 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[3]. +*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[3]. @@ -30,11 +30,11 @@ 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[3]. +initialized by linkman:upscli_read_authconf_file[3]. SEE ALSO -------- -linkman:upscli_create_authconf[3], -linkman:upscli_read_authconf[3], linkman:upscli_find_authconf[3], +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 40375b4c1b..5dff6f0573 100644 --- a/docs/man/upscli_init.txt +++ b/docs/man/upscli_init.txt @@ -90,7 +90,7 @@ 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[3] to pass equivalent information +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.txt b/docs/man/upscli_read_authconf_file.txt similarity index 81% rename from docs/man/upscli_read_authconf.txt rename to docs/man/upscli_read_authconf_file.txt index 8bdfb5a0ce..08d5f7a54e 100644 --- a/docs/man/upscli_read_authconf.txt +++ b/docs/man/upscli_read_authconf_file.txt @@ -1,10 +1,10 @@ -UPSCLI_READ_AUTHCONF(3) -======================= +UPSCLI_READ_AUTHCONF_FILE(3) +============================ NAME ---- -upscli_read_authconf - Read the authentication configuration file +upscli_read_authconf_file - Read the authentication configuration file SYNOPSIS -------- @@ -12,13 +12,13 @@ SYNOPSIS ------ #include - int upscli_read_authconf(const char *filename, int fatal_errors); + int upscli_read_authconf_file(const char *filename, int fatal_errors); ------ DESCRIPTION ----------- -The *upscli_read_authconf()* function reads the specified 'filename' (which +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. @@ -54,11 +54,11 @@ within that section. RETURN VALUE ------------ -The *upscli_read_authconf()* function returns '1' on success, or '-1' if an +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[3], +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/tests/test_authconf.c b/tests/test_authconf.c index 80220cc614..706e956f57 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -68,12 +68,12 @@ int main(int argc, char **argv) fclose(f); #ifdef DEBUG - if (upscli_read_authconf(NULL, 0) != 1) { + if (upscli_read_authconf_file(NULL, 0) != 1) { fprintf(stderr, "INFO: Default read_authconf failed (no user/site-provided config found)\n"); } #endif - if (upscli_read_authconf(test_conf, 1) != 1) { + if (upscli_read_authconf_file(test_conf, 1) != 1) { fprintf(stderr, "read_authconf failed\n"); return 1; } @@ -93,7 +93,7 @@ int main(int argc, char **argv) /* 1. Global match (no specific section for this host) */ printf("Checking global match for '@somehost:port'...\n"); - ac = upscli_find_authconf(NULL, "somehost", "port"); + ac = upscli_find_authconf_item(NULL, "somehost", "port"); if (ac) { printf("Global match got user=%s\n", ac->user ? ac->user : "NULL"); if (ac->user && strcmp(ac->user, "globaluser") == 0) { @@ -109,7 +109,7 @@ int main(int argc, char **argv) /* 2. Host default match */ printf("Checking host default match for '@localhost:12345'\n"); - ac = upscli_find_authconf(NULL, "localhost", "12345"); + ac = upscli_find_authconf_item(NULL, "localhost", "12345"); if (ac && strcmp(ac->user, "hostuser") == 0 && ac->forcessl == 1 && ac->certverify == 1) { printf("Host default match OK\n"); } else { @@ -119,7 +119,7 @@ int main(int argc, char **argv) /* 3. Exact match */ printf("Checking exact match for 'admin@localhost:12345'\n"); - ac = upscli_find_authconf("admin", "localhost", "12345"); + ac = upscli_find_authconf_item("admin", "localhost", "12345"); if (ac) { printf("Exact match: got user=%s pass=%s forcessl=%d\n", ac->user ? ac->user : "NULL", @@ -142,7 +142,7 @@ int main(int argc, char **argv) /* 4. Non-exact match */ printf("Checking non-exact match for 'somebody@localhost:12345'\n"); - ac = upscli_find_authconf("somebody", "localhost", "12345"); + ac = upscli_find_authconf_item("somebody", "localhost", "12345"); if (ac) { printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", ac->user ? ac->user : "NULL", @@ -165,7 +165,7 @@ int main(int argc, char **argv) /* 5. Include match */ printf("Checking include match for '@otherhost'\n"); - ac = upscli_find_authconf(NULL, "otherhost", NULL); + ac = upscli_find_authconf_item(NULL, "otherhost", NULL); snprintf(buf, sizeof(buf), "@otherhost:%u", (unsigned int)NUT_PORT); if (ac && ac->section && strcmp(ac->section, buf) == 0 @@ -185,13 +185,13 @@ int main(int argc, char **argv) /* 6. No bogus hits */ printf("Checking NO match for '@otherhost:portnum' other than global section\n"); - ac = upscli_find_authconf(NULL, "otherhost", "portnum"); + ac = upscli_find_authconf_item(NULL, "otherhost", "portnum"); if (ac) { if (!(ac->section) || !*(ac->section)) { printf("No bogus match OK: got global section\n"); } else { printf("No bogus match FAILED: had a hit\n"); - upscli_dump_authconf(NULL, ac, 1); + upscli_dump_authconf_item(NULL, ac, 1); return 1; } } else { From 321bc9105699eab4550aa27fd4c1f73925449637 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 13 May 2026 17:16:37 +0200 Subject: [PATCH 025/108] clients/authconf.{c,h}, docs: introduce upscli_clone_authconf_item() and upscli_merge_authconf_item() functions [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 134 ++++++++++++++++++++++- clients/authconf.h | 6 + docs/man/Makefile.am | 11 +- docs/man/upscli_create_authconf_item.txt | 40 ++++++- docs/man/upscli_get_authconf_list.txt | 2 +- 5 files changed, 184 insertions(+), 9 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 270713caf7..1f0d72bdbf 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -63,8 +63,10 @@ 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) + if (!node) { + upsdebugx(1, "Failed to create nutauth configuration node for section '%s'", NUT_STRARG(section)); return NULL; + } if (section) { node->section = xstrdup(section); @@ -75,12 +77,140 @@ upscli_authconf_t *upscli_create_authconf_item(const char *section) 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; + + /* FIXME: normalize */ + if (section) + node->section = xstrdup(section); + else + node->section = source->section ? xstrdup(source->section) : NULL; + + if ((at = strchr(node->section, '@')) != NULL) { + if (at != node->section) { + node->user = (char*)xcalloc(at - node->section + 1, sizeof(char)); + memcpy(node->user, node->section, at - node->section); + } /* else keep explicitly not-set */ + } else { + 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->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) { + if (at != target->section) { + target->user = (char*)xcalloc(at - target->section + 1, sizeof(char)); + memcpy(target->user, target->section, at - target->section); + } else { + /* keep explicitly not-set */ + free(target->user); + target->user = NULL; + } + } else { + if ( (!(target->user) || !*(target->user) ) + && (source->user && *(source->user) ) + ) { + free(target->user); + target->user = xstrdup(source->user); + } + } + + if ( (!(target->pass) || !*(target->pass) ) + && (source->pass && *(source->pass) ) + ) { + free(target->pass); + target->pass = xstrdup(source->pass); + } + + if ( (!(target->certpath) || !*(target->certpath) ) + && (source->certpath && *(source->certpath) ) + ) { + free(target->certpath); + target->certpath = xstrdup(source->certpath); + } + + if ( (!(target->certfile) || !*(target->certfile) ) + && (source->certfile && *(source->certfile) ) + ) { + free(target->certfile); + target->certfile = xstrdup(source->certfile); + } + + if ( (!(target->certident) || !*(target->certident) ) + && (source->certident && *(source->certident) ) + ) { + free(target->certident); + target->certident = xstrdup(source->certident); + } + + if ( (!(target->certpasswd) || !*(target->certpasswd) ) + && (source->certpasswd && *(source->certpasswd) ) + ) { + free(target->certpasswd); + target->certpasswd = xstrdup(source->certpasswd); + } + + if ( (!(target->ssl_backend) || !*(target->ssl_backend) ) + && (source->ssl_backend && *(source->ssl_backend) ) + ) { + free(target->ssl_backend); + target->ssl_backend = xstrdup(source->ssl_backend); + } + + 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(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'", section); + fatalx(EXIT_FAILURE, "Failed to create nutauth configuration node for section '%s' which should be added to the list", NUT_STRARG(section)); } /* Append to list */ diff --git a/clients/authconf.h b/clients/authconf.h index a703db0347..f8c01e5106 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -41,6 +41,12 @@ 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); diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 737701deb8..f2d5d6d2a4 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -834,9 +834,16 @@ 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_free_authconf_item.$(MAN_SECTION_API) +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_normalize_authconf_section_parts.$(MAN_SECTION_API) upscli_split_authconf_section.$(MAN_SECTION_API) + +UPSCLI_FIND_AUTHCONF_DEPS = \ + 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 $@ diff --git a/docs/man/upscli_create_authconf_item.txt b/docs/man/upscli_create_authconf_item.txt index 276f3ee2dc..57adea6417 100644 --- a/docs/man/upscli_create_authconf_item.txt +++ b/docs/man/upscli_create_authconf_item.txt @@ -4,8 +4,10 @@ UPSCLI_CREATE_AUTHCONF_ITEM(3) NAME ---- -upscli_create_authconf_item, upscli_free_authconf_item - Create or free an individual -authentication configuration item which is not necessarily in the list +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 -------- @@ -30,6 +32,10 @@ SYNOPSIS 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); ------ @@ -42,6 +48,28 @@ 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 strings with `xstrdup()` and copying the numeric flags. +If the 'section' argument is provided and contains a `user@` component, +the user field is (re-)set to that (or cleared and not cloned if empty +in the section title, e.g. if it starts with the `@` character). +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 not-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, the 'user' name field would be (re-)set to that, or cleared +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 @@ -51,8 +79,12 @@ internal list. RETURN VALUE ------------ -The *upscli_create_authconf_item()* function returns a pointer to the new structure, -or `NULL` if an error occurred. +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). diff --git a/docs/man/upscli_get_authconf_list.txt b/docs/man/upscli_get_authconf_list.txt index bf09ed4ea8..518fd0fcbb 100644 --- a/docs/man/upscli_get_authconf_list.txt +++ b/docs/man/upscli_get_authconf_list.txt @@ -23,7 +23,7 @@ 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[3]. +linkman:upscli_create_authconf_item[3]. RETURN VALUE ------------ From ddddceb9342446c6a54dc7ea59bc1959ca5ace1f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 08:41:50 +0200 Subject: [PATCH 026/108] clients/authconf.c: upscli_find_authconf_item(): refine return of global_defaults and zearch in empty list [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 1f0d72bdbf..72308ce602 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -938,16 +938,30 @@ upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, { 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 */ - /* Should we just return global_defaults? */ - upscli_authconf_t *tmp = authconf_list; - while (tmp) { - if (!tmp->section || !*(tmp->section)) { - return tmp; + 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; } - 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), From b07e607ee732f289e8bd0a629b156ac92b1472ad Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 17:45:50 +0200 Subject: [PATCH 027/108] clients/authconf.c: refactor with separation of upscli_add_authconf_item(name) and upscli_add_authconf(node) [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 72308ce602..0905b3cc15 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -205,15 +205,12 @@ upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_ return target; } -static upscli_authconf_t *upscli_add_authconf(const char *section) +static upscli_authconf_t *upscli_add_authconf(upscli_authconf_t* node) { - 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)); - } + if (!node) + return NULL; - /* Append to list */ + /* Append to end of list */ if (!authconf_list) { authconf_list = node; } else { @@ -227,6 +224,17 @@ static upscli_authconf_t *upscli_add_authconf(const char *section) 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) { @@ -751,7 +759,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) } if (!current_section) { - current_section = upscli_add_authconf(normalized_sect_name); + 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 user is set to this user */ @@ -825,7 +833,7 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) } else { /* Creating/modifying global defaults */ if (!global_defaults) { - global_defaults = upscli_add_authconf(NULL); + global_defaults = upscli_add_authconf_item(NULL); } /* Initial spec says global-scope includes may modify From fb25d7702464ed2b4f7cc070786b9cc6f66b012f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 18:36:20 +0200 Subject: [PATCH 028/108] clients/authconf.c, docs/man/upscli_create_authconf_item.txt: revise upscli_clone_authconf_item() and upscli_merge_authconf_item() functions vs. cloning of USER name [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 63 ++++++++++++------------ docs/man/upscli_create_authconf_item.txt | 9 ++-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 0905b3cc15..247f0885dd 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -95,12 +95,14 @@ upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const c else node->section = source->section ? xstrdup(source->section) : NULL; - if ((at = strchr(node->section, '@')) != NULL) { - if (at != node->section) { - node->user = (char*)xcalloc(at - node->section + 1, sizeof(char)); - memcpy(node->user, node->section, at - node->section); - } /* else keep explicitly not-set */ + 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; } @@ -127,68 +129,67 @@ upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_ return target; /* TOTHINK: (re-)normalize? */ - if ( (!(target->section) || !*(target->section) ) - && (source->section && *(source->section) ) + if ( (!(target->section) || !*(target->section)) + && (source->section && *(source->section)) ) { free(target->section); target->section = xstrdup(source->section); } - if ((at = strchr(target->section, '@')) != NULL) { - if (at != target->section) { - target->user = (char*)xcalloc(at - target->section + 1, sizeof(char)); - memcpy(target->user, target->section, at - target->section); - } else { - /* keep explicitly not-set */ - free(target->user); - target->user = NULL; - } + 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 { - if ( (!(target->user) || !*(target->user) ) - && (source->user && *(source->user) ) + /* No '@' or no username chars before it in target section title */ + if ( (!(target->user) || !*(target->user)) + && (source->user && *(source->user)) ) { free(target->user); target->user = xstrdup(source->user); - } + } /* else keep what was there */ } - if ( (!(target->pass) || !*(target->pass) ) - && (source->pass && *(source->pass) ) + if ( (!(target->pass) || !*(target->pass)) + && (source->pass && *(source->pass)) ) { free(target->pass); target->pass = xstrdup(source->pass); } - if ( (!(target->certpath) || !*(target->certpath) ) - && (source->certpath && *(source->certpath) ) + if ( (!(target->certpath) || !*(target->certpath)) + && (source->certpath && *(source->certpath)) ) { free(target->certpath); target->certpath = xstrdup(source->certpath); } - if ( (!(target->certfile) || !*(target->certfile) ) - && (source->certfile && *(source->certfile) ) + if ( (!(target->certfile) || !*(target->certfile)) + && (source->certfile && *(source->certfile)) ) { free(target->certfile); target->certfile = xstrdup(source->certfile); } - if ( (!(target->certident) || !*(target->certident) ) - && (source->certident && *(source->certident) ) + if ( (!(target->certident) || !*(target->certident)) + && (source->certident && *(source->certident)) ) { free(target->certident); target->certident = xstrdup(source->certident); } - if ( (!(target->certpasswd) || !*(target->certpasswd) ) - && (source->certpasswd && *(source->certpasswd) ) + if ( (!(target->certpasswd) || !*(target->certpasswd)) + && (source->certpasswd && *(source->certpasswd)) ) { free(target->certpasswd); target->certpasswd = xstrdup(source->certpasswd); } - if ( (!(target->ssl_backend) || !*(target->ssl_backend) ) - && (source->ssl_backend && *(source->ssl_backend) ) + if ( (!(target->ssl_backend) || !*(target->ssl_backend)) + && (source->ssl_backend && *(source->ssl_backend)) ) { free(target->ssl_backend); target->ssl_backend = xstrdup(source->ssl_backend); diff --git a/docs/man/upscli_create_authconf_item.txt b/docs/man/upscli_create_authconf_item.txt index 57adea6417..03c5d271a7 100644 --- a/docs/man/upscli_create_authconf_item.txt +++ b/docs/man/upscli_create_authconf_item.txt @@ -52,9 +52,8 @@ 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 strings with `xstrdup()` and copying the numeric flags. -If the 'section' argument is provided and contains a `user@` component, -the user field is (re-)set to that (or cleared and not cloned if empty -in the section title, e.g. if it starts with the `@` character). +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 @@ -62,8 +61,8 @@ The *upscli_merge_authconf_item()* function copies the values from the already have a value for the same field (non-NULL not-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, the 'user' name field would be (re-)set to that, or cleared -and not cloned. +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), From fa24aa6e7c85207824b92e6fbb96a14b31d9980c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 19:35:26 +0200 Subject: [PATCH 029/108] clients/authconf.c, docs/man/upscli_create_authconf_item.txt: revise upscli_merge_authconf_item() function vs. cloning of empty strings [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 36 ++++++------------------ docs/man/upscli_create_authconf_item.txt | 12 ++++---- 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 247f0885dd..5f7c683803 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -145,53 +145,33 @@ upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_ memcpy(target->user, target->section, at - target->section); } else { /* No '@' or no username chars before it in target section title */ - if ( (!(target->user) || !*(target->user)) - && (source->user && *(source->user)) - ) { - free(target->user); + if (!(target->user) && source->user) { target->user = xstrdup(source->user); } /* else keep what was there */ } - if ( (!(target->pass) || !*(target->pass)) - && (source->pass && *(source->pass)) - ) { - free(target->pass); + /* Replace only NULL strings; keep existing ones even if empty */ + if (!(target->pass) && source->pass) { target->pass = xstrdup(source->pass); } - if ( (!(target->certpath) || !*(target->certpath)) - && (source->certpath && *(source->certpath)) - ) { - free(target->certpath); + if (!(target->certpath) && source->certpath) { target->certpath = xstrdup(source->certpath); } - if ( (!(target->certfile) || !*(target->certfile)) - && (source->certfile && *(source->certfile)) - ) { - free(target->certfile); + if (!(target->certfile) && source->certfile) { target->certfile = xstrdup(source->certfile); } - if ( (!(target->certident) || !*(target->certident)) - && (source->certident && *(source->certident)) - ) { - free(target->certident); + if (!(target->certident) && source->certident) { target->certident = xstrdup(source->certident); } - if ( (!(target->certpasswd) || !*(target->certpasswd)) - && (source->certpasswd && *(source->certpasswd)) - ) { - free(target->certpasswd); + if (!(target->certpasswd) && source->certpasswd) { target->certpasswd = xstrdup(source->certpasswd); } - if ( (!(target->ssl_backend) || !*(target->ssl_backend)) - && (source->ssl_backend && *(source->ssl_backend)) - ) { - free(target->ssl_backend); + if (!(target->ssl_backend) && source->ssl_backend) { target->ssl_backend = xstrdup(source->ssl_backend); } diff --git a/docs/man/upscli_create_authconf_item.txt b/docs/man/upscli_create_authconf_item.txt index 03c5d271a7..2497f464e5 100644 --- a/docs/man/upscli_create_authconf_item.txt +++ b/docs/man/upscli_create_authconf_item.txt @@ -51,15 +51,15 @@ 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 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`. +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 not-empty string, or -a non-negative numeric flag). The `next` pointer is not modified. +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. From c17f67575ece857f2faef472416a70289f8ba475 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 21:41:11 +0200 Subject: [PATCH 030/108] docs/man/Makefile.am: fix after reshuffle nutauth.conf related methods vs population of man pages [#3329] Signed-off-by: Jim Klimov --- docs/man/Makefile.am | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index f2d5d6d2a4..155949ec46 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -1007,9 +1007,6 @@ upscli_normalize_authconf_section_parts.html: upscli_find_authconf_item.html upscli_split_authconf_section.html: upscli_find_authconf_item.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ -upscli_free_authconf_list.html: upscli_free_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 $? $@ From 796e766243a0fb3d1035cbef26cf35a8a53d495a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 09:01:47 +0200 Subject: [PATCH 031/108] clients/authconf.{c,h}: introduce upscli_get_authconf_item() for auto-merged items [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 184 ++++++++++++++++++++++--- clients/authconf.h | 12 +- docs/man/Makefile.am | 4 + docs/man/upscli_find_authconf_item.txt | 58 ++++++-- tests/test_authconf.c | 18 ++- 5 files changed, 241 insertions(+), 35 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 5f7c683803..a89c0d21b5 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -69,6 +69,7 @@ upscli_authconf_t *upscli_create_authconf_item(const char *section) } if (section) { + /* FIXME: normalize section */ node->section = xstrdup(section); } node->certverify = -1; @@ -89,10 +90,7 @@ upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const c if (source) { const char *at = NULL; - /* FIXME: normalize */ - if (section) - node->section = xstrdup(section); - else + if (!section) node->section = source->section ? xstrdup(source->section) : NULL; if ( ((at = strchr(node->section, '@')) != NULL) @@ -743,22 +741,19 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) 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 user is set to this 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); } - /* Copy global defaults to new section */ - if (global_defaults) { - if (!(current_section->user) && global_defaults->user) current_section->user = xstrdup(global_defaults->user); - if (global_defaults->pass) current_section->pass = xstrdup(global_defaults->pass); - if (global_defaults->certpath) current_section->certpath = xstrdup(global_defaults->certpath); - if (global_defaults->certfile) current_section->certfile = xstrdup(global_defaults->certfile); - if (global_defaults->certident) current_section->certident = xstrdup(global_defaults->certident); - if (global_defaults->certpasswd) current_section->certpasswd = xstrdup(global_defaults->certpasswd); - if (global_defaults->ssl_backend) current_section->ssl_backend = xstrdup(global_defaults->ssl_backend); - current_section->certverify = global_defaults->certverify; - current_section->forcessl = global_defaults->forcessl; - } + /* 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); @@ -1015,3 +1010,158 @@ upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, 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 */ + 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 */ + upscli_merge_authconf_item(global_defaults, retval); + } + + 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 index f8c01e5106..6c95040e11 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -86,11 +86,21 @@ int upscli_split_authconf_section(const char *sect_name, char **normalized_sect_host, char **normalized_sect_port); -/** Find the best matching authconf for a given connection string; +/** 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. * diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 155949ec46..49d61b281c 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -842,6 +842,7 @@ UPSCLI_CREATE_AUTHCONF_DEPS = \ 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) @@ -1001,6 +1002,9 @@ 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 $? $@ diff --git a/docs/man/upscli_find_authconf_item.txt b/docs/man/upscli_find_authconf_item.txt index d51753b444..3d138b2813 100644 --- a/docs/man/upscli_find_authconf_item.txt +++ b/docs/man/upscli_find_authconf_item.txt @@ -4,7 +4,8 @@ UPSCLI_FIND_AUTHCONF_ITEM(3) NAME ---- -upscli_find_authconf_item, upscli_normalize_authconf_section_parts, +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 @@ -15,11 +16,19 @@ 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, @@ -40,19 +49,42 @@ 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'. +'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) - -If a specific match is found, any missing fields in that section are -inherited from the global defaults. - -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 +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 @@ -82,5 +114,9 @@ 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_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/tests/test_authconf.c b/tests/test_authconf.c index 706e956f57..f571798ac5 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -93,7 +93,7 @@ int main(int argc, char **argv) /* 1. Global match (no specific section for this host) */ printf("Checking global match for '@somehost:port'...\n"); - ac = upscli_find_authconf_item(NULL, "somehost", "port"); + ac = upscli_get_authconf_item(NULL, "somehost", "port", 1); if (ac) { printf("Global match got user=%s\n", ac->user ? ac->user : "NULL"); if (ac->user && strcmp(ac->user, "globaluser") == 0) { @@ -109,7 +109,7 @@ int main(int argc, char **argv) /* 2. Host default match */ printf("Checking host default match for '@localhost:12345'\n"); - ac = upscli_find_authconf_item(NULL, "localhost", "12345"); + ac = upscli_get_authconf_item(NULL, "localhost", "12345", 1); if (ac && strcmp(ac->user, "hostuser") == 0 && ac->forcessl == 1 && ac->certverify == 1) { printf("Host default match OK\n"); } else { @@ -119,7 +119,7 @@ int main(int argc, char **argv) /* 3. Exact match */ printf("Checking exact match for 'admin@localhost:12345'\n"); - ac = upscli_find_authconf_item("admin", "localhost", "12345"); + 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", @@ -142,7 +142,7 @@ int main(int argc, char **argv) /* 4. Non-exact match */ printf("Checking non-exact match for 'somebody@localhost:12345'\n"); - ac = upscli_find_authconf_item("somebody", "localhost", "12345"); + ac = upscli_get_authconf_item("somebody", "localhost", "12345", 0); if (ac) { printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", ac->user ? ac->user : "NULL", @@ -158,6 +158,7 @@ int main(int argc, char **argv) printf("Non-exact match FAILED (wrong values): expecting user='%s' pass=\n", "somebody"); return 1; } + upscli_free_authconf_item(ac); } else { printf("Non-exact match FAILED (no ac)\n"); return 1; @@ -165,7 +166,7 @@ int main(int argc, char **argv) /* 5. Include match */ printf("Checking include match for '@otherhost'\n"); - ac = upscli_find_authconf_item(NULL, "otherhost", NULL); + 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 @@ -185,7 +186,7 @@ int main(int argc, char **argv) /* 6. No bogus hits */ printf("Checking NO match for '@otherhost:portnum' other than global section\n"); - ac = upscli_find_authconf_item(NULL, "otherhost", "portnum"); + ac = upscli_get_authconf_item(NULL, "otherhost", "portnum", 1); if (ac) { if (!(ac->section) || !*(ac->section)) { printf("No bogus match OK: got global section\n"); @@ -198,6 +199,11 @@ int main(int argc, char **argv) printf("No bogus match kind of OK: got no ac\n"); } + 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); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + upscli_free_authconf_list(); unlink(test_conf); unlink(include_conf); From aba9ceaf7d2399c8f14607b280860efb01a30925 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 19:07:07 +0200 Subject: [PATCH 032/108] tests/test_authconf.c: print TAP-style progress trackingl add tests for upscli_get_authconf_item() modification of the list [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 182 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 148 insertions(+), 34 deletions(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index f571798ac5..d61207816c 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -24,10 +24,10 @@ 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; + upscli_authconf_t *ac, *ac5, *ac7, *ac8, *ac9, *ac12; size_t num_sections; char buf[512], *s; - int l; + int l, testnum = 0; s = getenv("NUT_DEBUG_LEVEL"); if (s && str_to_int(s, &l, 10) && l > 0) { @@ -73,51 +73,59 @@ int main(int argc, char **argv) } #endif + /* 1. Expected file read */ if (upscli_read_authconf_file(test_conf, 1) != 1) { - fprintf(stderr, "read_authconf failed\n"); + 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); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); + /* 3. Expected printout 2 */ printf("=== Parsed configuration (debug view):\n"); /* With "for_debug", show all fields (highlight NULLs) */ num_sections = upscli_dump_authconf_list(NULL, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); /* Test matching */ printf("=== Testing matches...\n"); - /* 1. Global match (no specific section for this host) */ - printf("Checking global match for '@somehost:port'...\n"); + /* 4. Global match (no specific section for this host) */ + printf("Checking global match for '@somehost:port', and adding it to the list...\n"); ac = upscli_get_authconf_item(NULL, "somehost", "port", 1); if (ac) { printf("Global match got user=%s\n", ac->user ? ac->user : "NULL"); if (ac->user && strcmp(ac->user, "globaluser") == 0) { - printf("Global match OK\n"); + printf("ok %d - Global match OK\n", ++testnum); } else { - printf("Global match FAILED (wrong user)\n"); + printf("not ok %d - Global match FAILED (wrong user)\n", ++testnum); return 1; } } else { - printf("Global match FAILED (no ac)\n"); + printf("not ok %d - Global match FAILED (no ac)\n", ++testnum); return 1; } - /* 2. Host default match */ - printf("Checking host default match for '@localhost:12345'\n"); - ac = upscli_get_authconf_item(NULL, "localhost", "12345", 1); - if (ac && strcmp(ac->user, "hostuser") == 0 && ac->forcessl == 1 && ac->certverify == 1) { - printf("Host default match OK\n"); + /* 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("Host default match FAILED\n"); + printf("not ok %d - Host default match FAILED\n", ++testnum); + if (ac5) + upscli_free_authconf_item(ac5); return 1; } - /* 3. Exact match */ + /* 6. Exact match */ printf("Checking exact match for 'admin@localhost:12345'\n"); ac = upscli_get_authconf_item("admin", "localhost", "12345", 1); if (ac) { @@ -130,19 +138,78 @@ int main(int argc, char **argv) && ac->pass && strcmp(ac->pass, "adminpass") == 0 && ac->forcessl == 1 ) { - printf("Exact match OK\n"); + printf("ok %d - Exact match OK\n", ++testnum); } else { - printf("Exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", "admin", "adminpass"); + printf("not ok %d - Exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "admin", "adminpass"); return 1; } } else { - printf("Exact match FAILED (no ac)\n"); + printf("not ok %d - Exact match FAILED (no ac)\n", ++testnum); return 1; } - /* 4. Non-exact match */ + /* 7. Non-exact match */ printf("Checking non-exact match for 'somebody@localhost:12345'\n"); - ac = upscli_get_authconf_item("somebody", "localhost", "12345", 0); + ac7 = upscli_get_authconf_item("somebody", "localhost", "12345", 0); + if (ac7) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac7->user ? ac7->user : "NULL", + ac7->pass ? ac7->pass : "NULL", + ac7->forcessl); + + if (ac7->user && strcmp(ac7->user, "somebody") == 0 + && ac7->pass && strcmp(ac7->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac7->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 8. Host default match, saved to list */ + printf("Checking host default match for '@localhost:12345' and saving into list\n"); + ac8 = upscli_get_authconf_item(NULL, "localhost", "12345", 1); + if (ac8 && strcmp(ac8->user, "hostuser") == 0 && ac8->forcessl == 1 && ac8->certverify == 1) { + printf("ok %d - Host default match OK\n", ++testnum); + } else { + printf("not ok %d - Host default match FAILED\n", ++testnum); + return 1; + } + + /* 9. Non-exact match, take 2 */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, and adding it to the list\n"); + ac9 = upscli_get_authconf_item("somebody", "localhost", "12345", 1); + if (ac9) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac9->user ? ac9->user : "NULL", + ac9->pass ? ac9->pass : "NULL", + ac9->forcessl); + + if (ac9->user && strcmp(ac9->user, "somebody") == 0 + && ac9->pass && strcmp(ac9->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac9->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 10. Same non-exact match */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, should be same pointer\n"); + ac = upscli_get_authconf_item("somebody", "localhost", "12345", 1); if (ac) { printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", ac->user ? ac->user : "NULL", @@ -150,21 +217,59 @@ int main(int argc, char **argv) ac->forcessl); if (ac->user && strcmp(ac->user, "somebody") == 0 - && !ac->pass + && ac->pass && strcmp(ac->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ && ac->forcessl == 1 ) { - printf("Non-exact match OK\n"); + 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("Non-exact match FAILED (wrong values): expecting user='%s' pass=\n", "somebody"); + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); return 1; } - upscli_free_authconf_item(ac); } else { - printf("Non-exact match FAILED (no ac)\n"); + 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; } - /* 5. Include match */ + /* 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); @@ -172,37 +277,46 @@ int main(int argc, char **argv) && ac->section && strcmp(ac->section, buf) == 0 && ac->user && strcmp(ac->user, "otheruser") == 0 ) { - printf("Include match OK\n"); + printf("ok %d - Include match OK\n", ++testnum); } else { if (ac) { - printf("Include match FAILED: got section=%s user=%s\n", + 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("Include match FAILED: no ac\n"); + printf("not ok %d - Include match FAILED: no ac\n", ++testnum); } return 1; } - /* 6. No bogus hits */ + /* 15. No bogus hits */ printf("Checking NO match for '@otherhost:portnum' other than global section\n"); - ac = upscli_get_authconf_item(NULL, "otherhost", "portnum", 1); + ac = upscli_find_authconf_item(NULL, "otherhost", "portnum"); if (ac) { if (!(ac->section) || !*(ac->section)) { - printf("No bogus match OK: got global section\n"); + printf("ok %d - No bogus match OK: got global section\n", ++testnum); } else { - printf("No bogus match FAILED: had a hit\n"); + printf("not ok %d - No bogus match FAILED: had a hit\n", ++testnum); upscli_dump_authconf_item(NULL, ac, 1); return 1; } } else { - printf("No bogus match kind of OK: got no ac\n"); + printf("ok %d - No bogus match kind of OK: got no ac\n", ++testnum); } + /* 16. Expected printout 3 */ printf("=== Parsed configuration (production view) after several 'get' operations with results caching:\n"); /* Not "for_debug", but how would this info look in a config file */ num_sections = upscli_dump_authconf_list(NULL, 0); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + /* Added '@somehost:port' and 'somebody@...' */ + printf("%sok %d - parsed 6 sections\n", num_sections == 6 ? "" : "not ", ++testnum); + + upscli_free_authconf_item(ac5); + upscli_free_authconf_item(ac7); + upscli_free_authconf_item(ac12); + /* do not free ac8 and ac9 - they are added to list */ upscli_free_authconf_list(); unlink(test_conf); From 590f99057bd98c1cad8aaec572af4e9eb450e59a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 14 May 2026 21:07:04 +0200 Subject: [PATCH 033/108] clients/authconf.c: upscli_get_authconf_item(): add ifdef-ed away very optional debug messages [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clients/authconf.c b/clients/authconf.c index a89c0d21b5..16f2ce7c63 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -1139,14 +1139,26 @@ upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, 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__); From 8aa6886477a1c52a53bb666be64e264b4b378d06 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 15 May 2026 01:35:39 +0200 Subject: [PATCH 034/108] clients/authconf.{c,h}, docs/man/upscli_create_authconf_item.txt, docs/man/nutauth.conf.txt: add CERTHOST field support [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 14 ++++++++++++++ clients/authconf.h | 1 + docs/man/nutauth.conf.txt | 6 ++++++ docs/man/upscli_create_authconf_item.txt | 1 + 4 files changed, 22 insertions(+) diff --git a/clients/authconf.c b/clients/authconf.c index 16f2ce7c63..299239f274 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -111,6 +111,7 @@ upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const c 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; } @@ -173,6 +174,10 @@ upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_ 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; } @@ -227,6 +232,7 @@ upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node) free(node->certident); free(node->certpasswd); free(node->ssl_backend); + free(node->certhost); free(node); @@ -353,6 +359,11 @@ int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, in 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; @@ -425,6 +436,9 @@ static void set_authconf_val(upscli_authconf_t *conf, const char *var, const cha } 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")) diff --git a/clients/authconf.h b/clients/authconf.h index 6c95040e11..f846d304a0 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -29,6 +29,7 @@ typedef struct upscli_authconf_s { 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 */ diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt index 95df04c1a1..1b70835dfa 100644 --- a/docs/man/nutauth.conf.txt +++ b/docs/man/nutauth.conf.txt @@ -144,6 +144,12 @@ The following keywords are supported in both global scope and within sections: 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). diff --git a/docs/man/upscli_create_authconf_item.txt b/docs/man/upscli_create_authconf_item.txt index 2497f464e5..2040441c74 100644 --- a/docs/man/upscli_create_authconf_item.txt +++ b/docs/man/upscli_create_authconf_item.txt @@ -24,6 +24,7 @@ SYNOPSIS 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 */ From 0776efd3e2ae987790d9671eb27d76a27170911a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 15 May 2026 03:30:32 +0200 Subject: [PATCH 035/108] clients/authconf.c, tests/test_authconf.c: handle_authconf_args(): when we finish parsing a section, upscli_add_host_cert() if applicable [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 23 +++++++++++++++++++++++ tests/test_authconf.c | 2 ++ 2 files changed, 25 insertions(+) diff --git a/clients/authconf.c b/clients/authconf.c index 299239f274..b7f907acba 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -13,6 +13,7 @@ #include "authconf.h" #include "parseconf.h" +#include "upsclient.h" #include #include @@ -694,6 +695,28 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) 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_cert( + sect_host, + current_section->certhost, + current_section->certverify, + current_section->forcessl); + } + } + current_section_ignored = 0; sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ diff --git a/tests/test_authconf.c b/tests/test_authconf.c index d61207816c..6d82655579 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -65,6 +65,7 @@ int main(int argc, char **argv) } fprintf(f, "[@otherhost]\n"); fprintf(f, " USER = otheruser\n"); + fprintf(f, " CERTHOST = \"Other Server\"\n"); fclose(f); #ifdef DEBUG @@ -276,6 +277,7 @@ int main(int argc, char **argv) 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 { From 4525ec20396347b0925933b08ed7e2f9101d51f7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 15 May 2026 04:29:27 +0200 Subject: [PATCH 036/108] clients/upsclient.{c,h}, clients/authconf.c, docs: introduce upscli_add_host_port_cert() et al [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 3 +- clients/upsclient.c | 122 +++++++++++++++++++++++++++--- clients/upsclient.h | 2 + docs/man/Makefile.am | 5 ++ docs/man/upscli_add_host_cert.txt | 15 +++- 5 files changed, 134 insertions(+), 13 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index b7f907acba..f03fcbb3b9 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -709,8 +709,9 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) && sect_host && *sect_host && sect_port && *sect_port ) { - upscli_add_host_cert( + upscli_add_host_port_cert( sect_host, + (uint16_t)atol(sect_port), current_section->certhost, current_section->certverify, current_section->forcessl); diff --git a/clients/upsclient.c b/clients/upsclient.c index a3381784dd..fd8038d10f 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -157,13 +157,17 @@ static struct { typedef struct HOST_CERT_s { const char *host; + uint16_t port; const char *certname; int certverify; int forcessl; struct HOST_CERT_s *next; } HOST_CERT_t; +#if 0 static HOST_CERT_t* upscli_find_host_cert(const char* hostname); +#endif +static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t port); /* Flag for SSL init */ static int upscli_initialized = 0; @@ -341,15 +345,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:"", + (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) : NULL; if (cert != NULL) { - return cert->certverify==0 ? SECSuccess : SECFailure; + return cert->certverify==0 ? SECSuccess : SECFailure; } else { return verify_certificate==0 ? SECSuccess : SECFailure; } @@ -760,7 +765,7 @@ int upscli_init(int certverify, const char *certpath, /* NOTE: Maybe eventually these two methods below will invert: * who is implementation of whom. * TODO: Consider a method that parses our collection from - * upscli_get_authconf_list() to upscli_add_host_cert() and + * 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. */ @@ -1087,17 +1092,79 @@ int upscli_init2(int certverify, const char *certpath, } void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl) +{ + const char *s_port = strchr(hostname, ':'); + uint16_t port = NUT_PORT; + char host[LARGEBUF]; + + if (s_port) { + snprintf(host, + MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), + "%s", hostname); + if (s_port[1]) { +#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(s_port+1); + + if (l > 0 && l <= UINT16_MAX) { + port = (uint16_t)l; + } else { + struct servent *se = getservbyname(s_port + 1, "tcp"); + if (se && se->s_port > 0 && se->s_port <= UINT16_MAX) { + port = 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 + } + } + + upscli_add_host_port_cert( + s_port ? host : hostname, + 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; cert->host = xstrdup(hostname); + cert->port = port ? port : NUT_PORT; cert->certname = xstrdup(certname); cert->certverify = certverify; cert->forcessl = forcessl; first_host_cert = cert; #else NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); NUT_UNUSED_VARIABLE(certname); NUT_UNUSED_VARIABLE(certverify); NUT_UNUSED_VARIABLE(forcessl); @@ -1106,13 +1173,16 @@ 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) { #if defined(WITH_OPENSSL) || defined(WITH_NSS) HOST_CERT_t* cert = first_host_cert; if (hostname != NULL) { while (cert != NULL) { - if (cert->host != NULL && strcmp(cert->host, hostname)==0 ) { + if (cert->host != NULL + && strcmp(cert->host, hostname) == 0 + && cert->port == port + ) { return cert; } cert = cert->next; @@ -1120,12 +1190,44 @@ static HOST_CERT_t* upscli_find_host_cert(const char* hostname) } #else NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); 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) +{ + const char *s_port = strchr(hostname, ':'); + uint16_t port = NUT_PORT; + char host[LARGEBUF]; + + if (s_port) { + snprintf(host, + MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), + "%s", hostname); + if (s_port[1]) { + long l = atol(s_port+1); + + if (l > 0 && l <= UINT16_MAX) { + port = (uint16_t)l; + } else { + struct servent *se = getservbyname(s_port + 1, "tcp"); + if (se && se->s_port > 0 && se->s_port <= UINT16_MAX) { + port = se->s_port; + } + } + } + } + + return upscli_find_host_port_cert( + s_port ? host : hostname, + port); +} +#endif + int upscli_cleanup(void) { #ifdef WITH_OPENSSL @@ -1684,7 +1786,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); if (cert != NULL && cert->certname != NULL) { /* We have a setting like upsmon CERTHOST - to pin the certificate @@ -1894,7 +1996,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) #pragma GCC diagnostic pop #endif - cert = upscli_find_host_cert(ups->host); + cert = upscli_find_host_port_cert(ups->host, ups->port); if (cert != NULL && cert->certname != NULL) { upslogx(LOG_INFO, "Connecting in SSL to '%s' and look at certificate called '%s'", ups->host, cert->certname); @@ -2169,7 +2271,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); if (hostcert != NULL) { /* An host security rule is specified. */ diff --git a/clients/upsclient.h b/clients/upsclient.h index 221de62a9c..4f47c9151b 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -160,6 +160,8 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags /* blocking unless default timeout is specified, see also: upscli_init_default_connect_timeout() */ int upscli_connect(UPSCONN_t *ups, const char *host, uint16_t port, int flags); +void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* certname, int certverify, int forcessl); +/* hostname may be a host:port */ void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl); /* --- functions that only use the new names --- */ diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 49d61b281c..ea70ae9013 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -730,6 +730,7 @@ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS): upscli_upslog_set_debug_level.$(MAN_SECTI INST_MAN_DEV_API_PAGES = \ upsclient.$(MAN_SECTION_API) \ upscli_add_host_cert.$(MAN_SECTION_API) \ + $(UPSCLI_ADD_HOST_CERT_DEPS) \ upscli_cleanup.$(MAN_SECTION_API) \ upscli_connect.$(MAN_SECTION_API) \ upscli_tryconnect.$(MAN_SECTION_API) \ @@ -819,6 +820,10 @@ UPSCLI_INIT_DEPS = upscli_init2.$(MAN_SECTION_API) upscli_init_authconf.$(MAN_SE $(UPSCLI_INIT_DEPS): upscli_init.$(MAN_SECTION_API) touch $@ +UPSCLI_ADD_HOST_CERT_DEPS = upscli_add_host_port_cert.$(MAN_SECTION_API) +$(UPSCLI_ADD_HOST_CERT_DEPS): upscli_add_host_cert.$(MAN_SECTION_API) + touch $@ + upscli_readline_timeout.$(MAN_SECTION_API): upscli_readline.$(MAN_SECTION_API) touch $@ diff --git a/docs/man/upscli_add_host_cert.txt b/docs/man/upscli_add_host_cert.txt index f5f2dcb79e..0847b028ce 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,23 @@ 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. +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 From 5f3e1442fbdc4084ffebc4d016fe0457771a0889 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 15 May 2026 09:56:08 +0200 Subject: [PATCH 037/108] clients/nutclient.{cpp,h}: add rudimentary support for SSLConfig_CERTHOST with a port number [#3329] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 33 +++++++++++++++++++++++---------- clients/nutclient.h | 17 ++++++++++++----- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 1e85779db3..27b416a615 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2571,23 +2571,28 @@ SSLConfig_CERTHOST::SSLConfig_CERTHOST( const std::string& host_addr, const std::string& cert_subj, int forcessl, - int certverify) + int certverify, + uint16_t port) : _host_addr(host_addr), _cert_subj(cert_subj), _forcessl(forcessl), - _certverify(certverify) + _certverify(certverify), + _port(port) { + // TODO: Parse apart possible "host:port" spelling, involve getservbyname() } SSLConfig_CERTHOST::SSLConfig_CERTHOST( const char *host_addr, const char *cert_subj, int forcessl, - int certverify) + int certverify, + uint16_t port) : _host_addr(host_addr ? host_addr : ""), _cert_subj(cert_subj ? cert_subj : ""), _forcessl(forcessl), - _certverify(certverify) + _certverify(certverify), + _port(port) { } @@ -2610,6 +2615,11 @@ const char *SSLConfig_CERTHOST::getHostAddr_c_str() const return _host_addr.empty() ? nullptr : _host_addr.c_str(); } +uint16_t SSLConfig_CERTHOST::getPort() const +{ + return _port; +} + const std::string& SSLConfig_CERTHOST::getCertSubj() const { return _cert_subj; @@ -2632,7 +2642,10 @@ int SSLConfig_CERTHOST::getCertVerify() const bool SSLConfig_CERTHOST::operator < (const SSLConfig_CERTHOST& other) const { - if (_cert_subj.empty() && other._cert_subj.empty()) return _host_addr < other._host_addr; + if (_cert_subj.empty() && other._cert_subj.empty()) { + if (_host_addr == other._host_addr) return _port < other._port; + return _host_addr < other._host_addr; + } return _cert_subj < other._cert_subj; } @@ -2811,17 +2824,17 @@ const SSLConfig_CERTHOST *SSLConfig::getFirstCertHost() const return _certhosts.empty() ? nullptr : *(_certhosts.begin()); } -const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddr(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddr(const std::string &s, uint16_t port) const { for (const auto* item : _certhosts) { - if (item->getHostAddr() == s) { + if (item->getHostAddr() == s && (port == 0 || item->getPort() == 0 || item->getPort() == port)) { return item; } } return nullptr; } -const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(const std::string &s) const { for (const auto* item : _certhosts) { if (item->getCertSubj() == s) { @@ -2831,10 +2844,10 @@ const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(std::string &s) const return nullptr; } -const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddrOrSubj(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddrOrSubj(const std::string &s, uint16_t port) const { for (const auto* item : _certhosts) { - if (item->getHostAddr() == s || item->getCertSubj() == s) { + if ((item->getHostAddr() == s && (port == 0 || item->getPort() == 0 || item->getPort() == port)) || item->getCertSubj() == s) { return item; } } diff --git a/clients/nutclient.h b/clients/nutclient.h index e1c544aae9..e73ee53a57 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -402,17 +402,20 @@ class SSLConfig_CERTIDENT_NSS : public SSLConfig_CERTIDENT class SSLConfig_CERTHOST { public: + /** NOTE: Addr would be parsed into host:port and a 0 port may become NUT_PORT */ SSLConfig_CERTHOST( const std::string& host_addr, const std::string& cert_subj, int forcessl = -1, - int certverify = -1); + int certverify = -1, + uint16_t port = 0); SSLConfig_CERTHOST( const char *host_addr, const char *cert_subj, int forcessl = -1, - int certverify = -1); + int certverify = -1, + uint16_t port = 0); SSLConfig_CERTHOST& operator=(const SSLConfig_CERTHOST&) = default; SSLConfig_CERTHOST(const SSLConfig_CERTHOST&) = default; @@ -424,6 +427,8 @@ class SSLConfig_CERTHOST const std::string& getHostAddr() const; const char *getHostAddr_c_str() const; + uint16_t getPort() const; + const std::string& getCertSubj() const; const char *getCertSubj_c_str() const; @@ -438,6 +443,7 @@ class SSLConfig_CERTHOST std::string _cert_subj; int _forcessl; int _certverify; + uint16_t _port; }; /** @@ -496,9 +502,10 @@ class SSLConfig /** Simplify workflow for single-server connections */ const SSLConfig_CERTHOST *getFirstCertHost() const; - const SSLConfig_CERTHOST *getCertHostByAddr(std::string &s) const; - const SSLConfig_CERTHOST *getCertHostBySubj(std::string &s) const; - const SSLConfig_CERTHOST *getCertHostByAddrOrSubj(std::string &s) const; + /** NOTE: Addr would be parsed into host:port and a 0 port may become NUT_PORT */ + const SSLConfig_CERTHOST *getCertHostByAddr(const std::string &s, uint16_t port = 0) const; + const SSLConfig_CERTHOST *getCertHostBySubj(const std::string &s) const; + const SSLConfig_CERTHOST *getCertHostByAddrOrSubj(const std::string &s, uint16_t port = 0) const; /** Callback to apply this configuration into a TcpClient instance * (and further propagate into a Socket instance used by it). From dfb86c2438205c236b2882a168622c33299cc67c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 15 May 2026 11:54:55 +0200 Subject: [PATCH 038/108] clients/authconf.c, docs/man/upscli_read_authconf_file.txt: add support for NUT_AUTHCONF_FILE and/or NUT_AUTHCONF_PATH envvars to locate exactly one nutauth.conf candidate or bail out [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 25 +++++++++++++++++++++++++ docs/man/upscli_read_authconf_file.txt | 21 ++++++++++++--------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index f03fcbb3b9..c53b085645 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -913,6 +913,31 @@ int upscli_read_authconf_file(const char *filename, int fatal_errors) 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) { diff --git a/docs/man/upscli_read_authconf_file.txt b/docs/man/upscli_read_authconf_file.txt index 08d5f7a54e..7b37e5d0b9 100644 --- a/docs/man/upscli_read_authconf_file.txt +++ b/docs/man/upscli_read_authconf_file.txt @@ -18,15 +18,18 @@ SYNOPSIS 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 tries to locate either a 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 *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, From c26e681d8b8f76c0d78e805f5efecce213a23dd9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 08:59:08 +0200 Subject: [PATCH 039/108] clients/upsclient.c: upscli_add_host_cert(), upscli_find_host_cert(): refactor with shared get_port_from_string() logic [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 70 ++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index fd8038d10f..2f4fa03bbb 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1091,17 +1091,11 @@ int upscli_init2(int certverify, const char *certpath, return 1; } -void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl) +static uint16_t get_port_from_string(const char *str_port) { - const char *s_port = strchr(hostname, ':'); - uint16_t port = NUT_PORT; - char host[LARGEBUF]; + uint16_t retval = 0; - if (s_port) { - snprintf(host, - MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), - "%s", hostname); - if (s_port[1]) { + 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 @@ -1127,22 +1121,47 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve #pragma clang diagnostic ignored "-Wtautological-compare" #pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" #endif - long l = atol(s_port+1); + long l = atol(str_port); - if (l > 0 && l <= UINT16_MAX) { - port = (uint16_t)l; - } else { - struct servent *se = getservbyname(s_port + 1, "tcp"); - if (se && se->s_port > 0 && se->s_port <= UINT16_MAX) { - port = se->s_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) +{ + const char *s_port = strchr(hostname, ':'); + uint16_t port = NUT_PORT; + char host[LARGEBUF]; + + if (s_port) { + snprintf(host, + MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), + "%s", hostname); + + if (s_port[1]) { + port = get_port_from_string(s_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, s_port + 1); + port = NUT_PORT; + } } } @@ -1208,16 +1227,15 @@ static HOST_CERT_t* upscli_find_host_cert(const char* hostname) snprintf(host, MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), "%s", hostname); - if (s_port[1]) { - long l = atol(s_port+1); - if (l > 0 && l <= UINT16_MAX) { - port = (uint16_t)l; - } else { - struct servent *se = getservbyname(s_port + 1, "tcp"); - if (se && se->s_port > 0 && se->s_port <= UINT16_MAX) { - port = se->s_port; - } + if (s_port[1]) { + port = get_port_from_string(s_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, s_port + 1); + port = NUT_PORT; } } } From b83432c4f3cf61c1246b06428e288df64d6cb06b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 09:05:27 +0200 Subject: [PATCH 040/108] tests/NIT/nit.sh: add support for nutauth.conf file testing, add a read-only user [#3329, #1711, #3411] Call new generatecfg_nutauth() after we do generatecfg_upsdusers_trivial() (and "coincidentally" in one case generatecfg_upsd_add_SSL() as well). Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 170 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 97c66ed42f..0f1c6befc8 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -841,7 +841,11 @@ NUT_STATEPATH="${TESTDIR}/run" NUT_PIDPATH="${TESTDIR}/run" NUT_ALTPIDPATH="${TESTDIR}/run" NUT_CONFPATH="${TESTDIR}/etc" -export NUT_STATEPATH NUT_PIDPATH NUT_ALTPIDPATH NUT_CONFPATH +# Leave no ambiguity as to which nutauth.conf should be read by default: +# "${NUT_CONFPATH}/nutauth.conf" (e.g. not fall back to user or site configs). +# Only apply it after (re-)generating via generatecfg_nutauth() though: +NUT_AUTHCONF_FILE="none" +export NUT_STATEPATH NUT_PIDPATH NUT_ALTPIDPATH NUT_CONFPATH NUT_AUTHCONF_FILE if [ -f "${NUT_CONFPATH}/NIT.env-sandbox-ready" ] ; then log_warn "'${NUT_CONFPATH}/NIT.env-sandbox-ready' exists, do you have another instance of the script still running?" @@ -2165,6 +2169,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 +2181,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 @@ -2415,6 +2424,163 @@ EOF export NUT_QUIET_INIT_SSL } +### nutauth.conf: ############################################# + +generatecfg_nutauth() { + # NOTE: Tools will by default read from whatever "${NUT_AUTHCONF_FILE}" + # resolves to, but here we populate the tests' instance (not overwrite + # some user configuration file, if that is somehow supplied)! + { cat << EOF +# Global section for nutauth.conf, inherited and overridden per line by others +EOF + + case "${WITH_SSL_CLIENT}" in + none) ;; + OpenSSL) + log_info "Adding ${WITH_SSL_CLIENT} client-side SSL config to nutauth.conf" + cat << EOF +# OpenSSL CERTFILE: PEM file with client cert, possibly the +# intermediate and root CA's, and finally corresponding private key +CERTFILE = "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}upsmon.pem" + +# OpenSSL CERTPATH: Directory with PEM file(s), looked up by the +# CA subject name hash value (which must include our NUT server). +# Here we just use the path for PEM file that should be populated +# by the generatecfg_upsd_add_SSL() method. +CERTPATH = "${TESTCERT_PATH_ROOTCA}" +EOF + ;; + NSS) + log_info "Adding ${WITH_SSL_CLIENT} client-side SSL config to nutauth.conf" + cat << EOF +# NSS CERTPATH: Directory with 3-file database of cert/key store +CERTPATH = "${TESTCERT_PATH_CLIENT}" +EOF + ;; + esac + + # Shared features for both SSL backends: + case x"${WITH_SSL_CLIENT_CERTIDENT}" in + x"name+pass") + cat << EOF +# SSL enabled, our cert nickname and private key password: Who am I? +CERTIDENT_NAME = "${TESTCERT_CLIENT_NAME}" +CERTIDENT_PASS = "${TESTCERT_CLIENT_PASS}" +EOF + ;; + x"name") # Really unlikely + cat << EOF +# SSL enabled, our cert nickname and private key password: Who am I? +CERTIDENT_NAME = "${TESTCERT_CLIENT_NAME}" +# A really unlikely case: this backend does not support passphrases?.. +CERTIDENT_PASS = "" +EOF + ;; + x"pass") + cat << EOF +# SSL enabled, our cert nickname and private key password: Who am I? +# This SSL backend can not check cert subject... +CERTIDENT_NAME = "" +# ...but at least can do private key passwords: +CERTIDENT_PASS = "${TESTCERT_CLIENT_PASS}" +EOF + ;; + esac + + case "${WITH_SSL_CLIENT}" in + none) + cat << EOF +# SSL not enabled: do not check server certs, do not require STARTTLS success: +CERTVERIFY = 0 +FORCESSL = 0 + +[@localhost:${NUT_PORT}] +EOF + ;; + OpenSSL|NSS) + cat << EOF +# Defaults that CERTHOST may override per-server, but note +# that this impacts also the general NUT client behavior. +# 0 for OK to fail => proceed in plaintext (should be overridden +# by the specific localhost definition below): +FORCESSL = 0 + +# -1 for inheriting a better value elsewhere, e.g. in host +# definition below, or effectively 0 if never defined exactly: +CERTVERIFY = -1 + +[@localhost:${NUT_PORT}] # We also try different indentation and comment styles here +EOF + + if [ x"${WITH_SSL_SERVER}" != xnone ] ; then + case x"${WITH_SSL_CLIENT_CERTHOST}" in + x"none") cat << EOF + # Custom settings for a specific remote server: + CERTHOST = "${TESTCERT_SERVER_NAME}" +CERTVERIFY = 1 + FORCESSL = 0 +EOF + ;; + x"addr") cat << EOF + # Custom settings for a specific remote server without verifying + # the host cert for nickname '${TESTCERT_SERVER_NAME}': + # CERTHOST = "" +# Just verify the CA matches what we trust: +CERTVERIFY = 1 + FORCESSL = 1 +EOF + ;; + *) cat << EOF +# Custom settings for a specific remote server: +CERTHOST = "${TESTCERT_SERVER_NAME}" +CERTVERIFY = 1 + FORCESSL = 1 +EOF + ;; + esac + fi + ;; + esac + + # Previous clauses end somewhere in the [@localhost:${NUT_PORT}] section + # Keep credentials in sync with generatecfg_upsdusers_trivial() + cat << EOF + # Default credentials for access to this server + USER = 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 +2864,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 +2971,7 @@ generatecfg_sandbox() { generatecfg_upsd_nodev generatecfg_upsd_add_SSL generatecfg_upsdusers_trivial + generatecfg_nutauth generatecfg_ups_dummy } From 52981ee34481e53f07230801d9e2f1bda33a7fc6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 13:18:59 +0200 Subject: [PATCH 041/108] tests/test_authconf.c: consult presence of NUT_AUTHCONF_FILE envvar to parse and print out that file [#3329, #1711] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 6d82655579..6e2f75ae5c 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -68,13 +68,20 @@ int main(int argc, char **argv) fprintf(f, " CERTHOST = \"Other Server\"\n"); fclose(f); -#ifdef DEBUG - if (upscli_read_authconf_file(NULL, 0) != 1) { - fprintf(stderr, "INFO: Default read_authconf failed (no user/site-provided config found)\n"); + 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); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + } } -#endif /* 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; From b9905a3aff0330f2f950b438ef82cd54ba9a90b7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 13:20:48 +0200 Subject: [PATCH 042/108] clients/authconf.c: handle_authconf_args(): revise logging of parsed lines, especially malformed and sensitive ones [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/clients/authconf.c b/clients/authconf.c index c53b085645..394ddb8aa8 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -836,10 +836,20 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) * 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) { From e3f9f44df8b71b67c650185316e6573eae2e3221 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 15:09:19 +0200 Subject: [PATCH 043/108] clients/upsclient.c: document better the upscli_init*() and upscli_sslinit() methods [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 57 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 2f4fa03bbb..7ff5bb51a9 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -755,19 +755,41 @@ static int openssl_cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) #endif -/* Legacy API, without support for client's own certificate in OpenSSL builds */ +/** Initialize SSL support with specific requirements. + * Call this or a related method before upscli_sslinit() to initiate STARTTLS + * in a connection to the server. + * + * Legacy API, without support for client's own certificate in OpenSSL builds. + * + * @see upscli_init_authconf() + * @see upscli_init2() + * @see upscli_sslinit() + * @see upscli_connect() + * @see upscli_tryconnect() + */ int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd) { return upscli_init2(certverify, certpath, certname, certpasswd, NULL); } -/* NOTE: Maybe eventually these two methods below will invert: - * who is implementation of whom. +/** 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) { @@ -777,6 +799,22 @@ int upscli_init_authconf(upscli_authconf_t *ac) 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) @@ -1704,10 +1742,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) { From 0e9d1c8a00fd7e2725a3cb365c7145c04bcb09a0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 16:06:33 +0200 Subject: [PATCH 044/108] clients/upsc.c, docs: add support for "-A /path/to/nutauth.conf" [#3329, #3411] Signed-off-by: Jim Klimov --- NEWS.adoc | 9 ++++++++ UPGRADING.adoc | 11 ++++++++++ clients/upsc.c | 52 ++++++++++++++++++++++++++++++++++++++++++++--- docs/man/upsc.txt | 27 +++++++++++++++++++++--- 4 files changed, 93 insertions(+), 6 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 27d6e3dd8e..d519aaff0d 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -129,6 +129,15 @@ https://github.com/networkupstools/nut/milestone/13 * Introduced support for "authconf" files to store and convey NUT client authentication details. [issue #3329] + - `upsc` 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 or to not even try reading one. [issues #3329, #3411] + - `upsmon` client updates: * Introduced support for `CERTFILE` option, so the client can identify itself to the data server also in OpenSSL builds. [issue #3331] diff --git a/UPGRADING.adoc b/UPGRADING.adoc index e4beed637a..503986cfec 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -46,6 +46,17 @@ Changes from 2.8.5 to 2.8.6 if the requested value is larger than what is allowed (minus some reserve for configuration files and other use-cases). [issue #3365] +- Enabled support for `nutauth.conf` files to provide credentials and/or + SSL settings in clients which previously only did best-effort attempts at + secure communications without an individual certificate, and only anonymously + for reading like `upsc`. ++ +The new `-A filename` option defaults to trying to use a `nutauth.conf` file + (if found in one of the default locations) but not failing if one is not + usable; specific values can require use of such a file or to not even try + reading one ('none' as the legacy default). See the updated manual pages + for more details. [issues #3329, #3411] + Changes from 2.8.4 to 2.8.5 --------------------------- diff --git a/clients/upsc.c b/clients/upsc.c index c2c002a3b0..da235a3d0d 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -39,12 +39,14 @@ #define builtin_setproctag(x) setproctag(x) #define setproctag(x) do { builtin_setproctag(x); upscli_upslog_setproctag(x, nut_common_cookie()); } while(0) -static char *upsname = NULL, *hostname = NULL; +static char *upsname = NULL, *hostname = NULL, + /* Note: nutauth is either NULL or points to an optarg, so is not freed */ + *nutauth = NULL; static UPSCONN_t *ups = NULL; static int output_json = 0; /* For getopt loops below: */ -static const char optstring[] = "+DhlLcVW:j"; +static const char optstring[] = "+DhlLcVW:jA:"; static void help(const char *prog) { @@ -76,6 +78,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -D - raise debugging level\n"); printf(" -h - display this help text\n"); @@ -406,6 +411,8 @@ int main(int argc, char **argv) int varlist = 0, clientlist = 0, verbose = 0; const char *prog = getprogname_argv0_default(argc > 0 ? argv[0] : NULL, "upsc"); const char *net_connect_timeout = NULL; + int flags_ssl = UPSCLI_CONN_TRYSSL; + char str_port[16]; NUT_UNUSED_VARIABLE(upslog_start_tmp); upscli_upslog_setprocname(xstrdup(getmyprocname()), nut_common_cookie()); @@ -455,6 +462,11 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 'L': verbose = 1; goto fallthrough_case_l; @@ -489,6 +501,28 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + /* Not passing fatal_errors=1 into the parser due to JSON support */ + int parsed = -1; + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + parsed = upscli_read_authconf_file(NULL, 0); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + parsed = upscli_read_authconf_file(nutauth, 0); + } + if (parsed < 0) { + fatalx_error_json_simple(0, "Failed to parse auth config file"); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { char msg[LARGEBUF]; snprintf(msg, sizeof(msg), "invalid network timeout: %s", net_connect_timeout); @@ -516,9 +550,21 @@ int main(int argc, char **argv) upsdebugx(1, "upsname='%s' hostname='%s' port='%" PRIu16 "'", NUT_STRARG(upsname), NUT_STRARG(hostname), port); + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + ups = (UPSCONN_t *)xmalloc(sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { fatalx_error_json_simple(0, upscli_strerror(ups)); } diff --git a/docs/man/upsc.txt b/docs/man/upsc.txt index 215ffc25c5..2d8beed448 100644 --- a/docs/man/upsc.txt +++ b/docs/man/upsc.txt @@ -41,13 +41,13 @@ OPTIONS *-l* 'host':: List all UPS names configured at 'host', one name per line. The hostname - defaults to "localhost". You may optionally add a colon and a port number. + defaults to `localhost`. You may optionally add a colon and a port number. *-L* 'host':: As above, list all UPS names configured at 'host', including their description provided by the remote linkman:upsd[8] from its linkman:ups.conf[5]. - The hostname defaults to "localhost". You may optionally add a colon and + The hostname defaults to `localhost`. You may optionally add a colon and a port number to override the default port. *-c* 'ups':: @@ -57,7 +57,7 @@ OPTIONS 'ups':: Display the status of that UPS. The format for this option is - 'upsname[@hostname[:port]]'. The default hostname is "localhost". + 'upsname[@hostname[:port]]'. The default hostname is `localhost`. 'variable':: @@ -89,6 +89,27 @@ COMMON OPTIONS indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + EXAMPLES -------- From 317d7456a7ad9bf16baf02f8523a0e6c5b6bd426 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 16:17:32 +0200 Subject: [PATCH 045/108] clients/authconf.{c,h}, docs/man/upscli_dump_authconf_item.txt, tests/test_authconf.c: extend upscli_dump_authconf_{list,item}() with an option to show/hide passwords [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 10 +++++----- clients/authconf.h | 4 ++-- docs/man/upscli_dump_authconf_item.txt | 16 ++++++++++++---- tests/test_authconf.c | 10 +++++----- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 394ddb8aa8..484478a4cb 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -296,7 +296,7 @@ static int upscli_dump_authconf_line_int(FILE *restrict stream, const char *var, return res; } -int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug) +int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug, int show_pass) { char *indent = NULL; int res = 0, ret = 0; @@ -330,7 +330,7 @@ int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, in return ret; ret += res; - res = upscli_dump_authconf_line_str(stream, "PASS", node->pass, indent, for_debug); + res = upscli_dump_authconf_line_str(stream, "PASS", show_pass || !(node->pass) ? node->pass : "", indent, for_debug); if (res < 0) return ret; ret += res; @@ -350,7 +350,7 @@ int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, in return ret; ret += res; - res = upscli_dump_authconf_line_str(stream, "CERTIDENT_PASS", node->certpasswd, indent, for_debug); + 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; @@ -378,14 +378,14 @@ int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, in return ret; } -size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug) +size_t upscli_dump_authconf_list(FILE *restrict 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); + upscli_dump_authconf_item(stream, node, for_debug, show_pass); node = node->next; } diff --git a/clients/authconf.h b/clients/authconf.h index f846d304a0..8abf507827 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -111,11 +111,11 @@ upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, * 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 *restrict stream, upscli_authconf_t *node, int for_debug); +int upscli_dump_authconf_item(FILE *restrict 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 *restrict stream, int for_debug); +size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug, int show_pass); #ifdef __cplusplus } diff --git a/docs/man/upscli_dump_authconf_item.txt b/docs/man/upscli_dump_authconf_item.txt index 77f452582f..176b02f70d 100644 --- a/docs/man/upscli_dump_authconf_item.txt +++ b/docs/man/upscli_dump_authconf_item.txt @@ -12,9 +12,11 @@ SYNOPSIS ------ #include - int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug); + int upscli_dump_authconf_item(FILE *restrict stream, + upscli_authconf_t *node, int for_debug, int show_pass); - size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug); + size_t upscli_dump_authconf_list(FILE *restrict stream, + int for_debug, int show_pass); ------ DESCRIPTION @@ -29,21 +31,27 @@ 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 ------------ diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 6e2f75ae5c..88c78c9e65 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -75,7 +75,7 @@ int main(int argc, char **argv) } else { printf("=== Parsed user configuration (debug view):\n"); /* With "for_debug", show all fields (highlight NULLs) */ - num_sections = upscli_dump_authconf_list(NULL, 1); + num_sections = upscli_dump_authconf_list(NULL, 1, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); } } @@ -91,14 +91,14 @@ int main(int argc, char **argv) /* 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); + num_sections = upscli_dump_authconf_list(NULL, 0, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); /* 3. Expected printout 2 */ printf("=== Parsed configuration (debug view):\n"); /* With "for_debug", show all fields (highlight NULLs) */ - num_sections = upscli_dump_authconf_list(NULL, 1); + num_sections = upscli_dump_authconf_list(NULL, 1, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); @@ -307,7 +307,7 @@ int main(int argc, char **argv) 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); + upscli_dump_authconf_item(NULL, ac, 1, 1); return 1; } } else { @@ -317,7 +317,7 @@ int main(int argc, char **argv) /* 16. Expected printout 3 */ printf("=== Parsed configuration (production view) after several 'get' operations with results caching:\n"); /* Not "for_debug", but how would this info look in a config file */ - num_sections = upscli_dump_authconf_list(NULL, 0); + num_sections = upscli_dump_authconf_list(NULL, 0, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); /* Added '@somehost:port' and 'somebody@...' */ printf("%sok %d - parsed 6 sections\n", num_sections == 6 ? "" : "not ", ++testnum); From 799211f924f74730bc30be74cf680b937f0a474f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 16:21:02 +0200 Subject: [PATCH 046/108] clients/upsclient.c: upscli_init_authconf(): debug-trace the authconf pointer [#3329, #1711] Signed-off-by: Jim Klimov --- clients/upsclient.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 7ff5bb51a9..577598c03a 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -793,8 +793,15 @@ int upscli_init(int certverify, const char *certpath, */ int upscli_init_authconf(upscli_authconf_t *ac) { - if (!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); + } return upscli_init2(ac->certverify, ac->certpath, ac->certident, ac->certpasswd, ac->certfile); } From ee128e9820827b0134a30a919d96c3735d3235e5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 16:45:20 +0200 Subject: [PATCH 047/108] clients/upsclient.c: upscli_init_authconf(): if the "ac" refers to a CERTHOST, call upscli_add_host_cert() on it [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/clients/upsclient.c b/clients/upsclient.c index 577598c03a..205e0485e1 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -803,6 +803,18 @@ int upscli_init_authconf(upscli_authconf_t *ac) 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); } @@ -1219,6 +1231,10 @@ void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* { #if defined(WITH_OPENSSL) || defined(WITH_NSS) HOST_CERT_t* cert = (HOST_CERT_t *)xmalloc(sizeof(HOST_CERT_t)); + + upsdebugx(1, "%s: adding CERTHOST: host '%s' port '%u' certname '%s' certverify %d forcessl %d", + __func__, hostname, (unsigned int)port, certname, certverify, forcessl); + cert->next = first_host_cert; cert->host = xstrdup(hostname); cert->port = port ? port : NUT_PORT; From 841e066a24f0059935be0435bc417f3027168f00 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 16:53:21 +0200 Subject: [PATCH 048/108] clients/upsclient.c: upscli_init2(): quiesce reaction to NUT_QUIET_INIT_SSL=false [#1711] Signed-off-by: Jim Klimov --- clients/upsclient.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 205e0485e1..b2e577e833 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -881,7 +881,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; } } From fe7e0716cfdfe72454e573e6676d940bc148af1d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 17:05:16 +0200 Subject: [PATCH 049/108] clients/upsclient.c: upscli_add_host_cert(): revise splitting apart the "hostname" which may come from authconf section name [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index b2e577e833..31eb8259c4 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1204,20 +1204,27 @@ static uint16_t get_port_from_string(const char *str_port) void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl) { - const char *s_port = strchr(hostname, ':'); + /* Support parsing apart authconf section names */ + const char *s_port = strchr(hostname, ':'), *s_host = strchr(hostname, '@'); uint16_t port = NUT_PORT; char host[LARGEBUF]; + if (s_host) { + s_host++; + } else { + s_host = hostname; + } + if (s_port) { snprintf(host, - MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), - "%s", hostname); + MIN(sizeof(host) - 1, (size_t)(s_port - s_host + 1)), + "%s", s_host); if (s_port[1]) { port = get_port_from_string(s_port + 1); if (port == 0) { upsdebugx(1, "%s: could not resolve port component '%s' " - "in hostname:port spec '%s' into a number, " + "in [user@]hostname:port spec '%s' into a number, " "falling back to standard NUT port", __func__, hostname, s_port + 1); port = NUT_PORT; @@ -1225,8 +1232,11 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve } } + upsdebugx(4, "%s: split '%s' into hostname '%s' port '%u'", + __func__, hostname, host, (unsigned int)port); + upscli_add_host_port_cert( - s_port ? host : hostname, + s_port ? host : s_host, port, certname, certverify, forcessl); } From 6f86ed0be6ff774a949f95545297fcbb0b6ff4ce Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 18 May 2026 20:14:24 +0200 Subject: [PATCH 050/108] clients/upsclient.c: upscli_find_host_port_cert(): debug-trace whether this succeeded [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clients/upsclient.c b/clients/upsclient.c index 31eb8259c4..0bfdd92c94 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1276,11 +1276,14 @@ static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t po && strcmp(cert->host, hostname) == 0 && cert->port == port ) { + upsdebugx(4, "%s: found '%s' for '%s':'%u'", + __func__, NUT_STRARG(cert->certname), hostname, (unsigned int)port); return cert; } cert = cert->next; } } + upsdebugx(4, "%s: nothing found for '%s':'%u'", __func__, hostname, (unsigned int)port); #else NUT_UNUSED_VARIABLE(hostname); NUT_UNUSED_VARIABLE(port); From ea6a4144a44ad9010d3d9c2494665911e32a5d4f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 03:03:39 +0200 Subject: [PATCH 051/108] clients/upsclient.c: BadCertHandler(): cast printing of port [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 0bfdd92c94..d66bcd8663 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -347,7 +347,7 @@ static SECStatus BadCertHandler(UPSCONN_t *arg, PRFileDesc *fd) upslogx(LOG_WARNING, "Certificate validation failed for %s:%" PRIu16, (arg&&arg->host)?arg->host:"", - (arg ? arg->port : NUT_PORT)); + (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. From e5a38179bfe5a95dd7c6ce4e21a8928286df4f4c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 03:04:57 +0200 Subject: [PATCH 052/108] clients/upsclient.c: upscli_add_host_cert(): fix reporting for plain legacy "hostname" input [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index d66bcd8663..50358aa6e5 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1230,10 +1230,14 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve port = NUT_PORT; } } + } else { + host[0] = '\0'; } upsdebugx(4, "%s: split '%s' into hostname '%s' port '%u'", - __func__, hostname, host, (unsigned int)port); + __func__, hostname, + s_port ? host : s_host, + (unsigned int)port); upscli_add_host_port_cert( s_port ? host : s_host, From 3aede075715719621c1b22d76deda5b294aea147 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 03:11:21 +0200 Subject: [PATCH 053/108] conf/upsmon.conf.sample.in, docs/man/upsmon.conf.txt: clarify CERTHOST with non-default NUT_PORT [#3329] Signed-off-by: Jim Klimov --- conf/upsmon.conf.sample.in | 3 +++ docs/man/upsmon.conf.txt | 7 +++++++ 2 files changed, 10 insertions(+) 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/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. From 25a2835f9e2de32a048675151f0dd6a0e26ae16c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 03:16:56 +0200 Subject: [PATCH 054/108] tests/NIT/nit.sh: generatecfg_upsmon_add_SSL(): consider CERTHOST with non-default NUT_PORT now [#3329] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 0f1c6befc8..4e1234427a 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2400,19 +2400,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 From fe057a0bf0017c2df83e68d50ecd0b69e6a2215a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 04:16:30 +0200 Subject: [PATCH 055/108] clients/upsclient.c: revise logging from AuthCertificate*() methods [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 50358aa6e5..0c6d9708d2 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -316,9 +316,11 @@ static SECStatus AuthCertificate(CERTCertDBHandle *arg, PRFileDesc *fd, UPSCONN_t *ups = (UPSCONN_t *)SSL_RevealPinArg(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"); + upslogx(LOG_INFO, "Intend to authenticate %s %s:%u : %s", + isServer ? "client" : "server", + ups ? ups->host : "", + (unsigned int)(ups ? ups->port : NUT_PORT), + status==SECSuccess ? "SUCCESS" : "FAILED"); if (status != SECSuccess) { nss_error(isServer ? "SSL_AuthCertificate(server)" : "SSL_AuthCertificate(client)"); @@ -335,8 +337,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; } From fc37889d3a83ea3eaba1cdb8885d51a3d9e00da0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 04:17:23 +0200 Subject: [PATCH 056/108] clients/upsclient.c: upscli_sslinit(): wrap long lines in NSS part [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 0c6d9708d2..76a830748d 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -2074,29 +2074,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; From 9c1fc9b11c18a06583f71ec5abe9dff03a5d8c25 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 05:39:28 +0200 Subject: [PATCH 057/108] clients/upsclient.c: AuthCertificate(), upscli_sslinit(): refactor NSS server certname validation [#3331] It is NOT about just host name (URL) matching per our spec. Signed-off-by: Jim Klimov --- clients/upsclient.c | 102 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 76a830748d..0697d7bf25 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -314,16 +314,87 @@ 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 %s %s:%u : %s", + 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); + 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 (status != SECSuccess) { - nss_error(isServer ? "SSL_AuthCertificate(server)" : "SSL_AuthCertificate(client)"); + if (peer) { + CERT_DestroyCertificate(peer); } return status; @@ -749,6 +820,12 @@ static int openssl_cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) upsdebugx(5, "%s: issuer=%s", __func__, bufCA); } +/* TOTHINK: Match by CN=...? (Here we are after clearing other error cases) : + // This is the counterpart's own cert : + if (depth == 0 && !preverify_ok) { + } +*/ + if (openssl_cert_verify_data->always_continue) { upsdebugx(4, "%s: requested to always continue, return ok=1 (not %d provided by caller): depth=%d:%s", __func__, preverify_ok, depth, buf); return 1; @@ -1817,7 +1894,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]; @@ -2110,23 +2186,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) #pragma GCC diagnostic pop #endif - cert = upscli_find_host_port_cert(ups->host, ups->port); - 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; From 7e4d094ad716629f3d5d9f22d5ba672be0413663 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 09:24:28 +0200 Subject: [PATCH 058/108] tests/NIT/nit.sh: do not export NUT_AUTHCONF_FILE to NIT.env [#3329, #1711] Depending on context, whether "none" or a real path may be useful or toxic to custom developer test works. Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 4e1234427a..92fb6d339f 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2029,7 +2029,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 From e2a98f75f5d45b7d9bbde874151a80d139885850 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 13:02:12 +0200 Subject: [PATCH 059/108] clients/authconf.c, docs/man/nutauth.conf.txt, tests/NIT/nit.sh: set_authconf_val(): support USERNAME as alias of USER [#3329] Follow the NUT Networked Protocol keywords a bit more closely. Signed-off-by: Jim Klimov --- clients/authconf.c | 2 +- docs/man/nutauth.conf.txt | 2 +- tests/NIT/nit.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 484478a4cb..684d643da2 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -410,7 +410,7 @@ static void set_authconf_val(upscli_authconf_t *conf, const char *var, const cha if (!conf || !var) return; - if (!strcasecmp(var, "user")) { + if (!strcasecmp(var, "user") || !strcasecmp(var, "username")) { if (current_section_with_fixed_username && conf->user && (!val || (val && strcmp(conf->user, val))) ) { diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt index 1b70835dfa..2cb2761e43 100644 --- a/docs/man/nutauth.conf.txt +++ b/docs/man/nutauth.conf.txt @@ -113,7 +113,7 @@ GLOBAL AND SECTION DIRECTIVES The following keywords are supported in both global scope and within sections: -*user* (case-insensitive token):: +*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 diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 92fb6d339f..ea51254773 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2546,7 +2546,7 @@ EOF # Keep credentials in sync with generatecfg_upsdusers_trivial() cat << EOF # Default credentials for access to this server - USER = reader + USERNAME = reader PASS = "$TESTPASS_READER" [admin@:${NUT_PORT}] From e110bc93759e1a289aa46769abb0c91d2f3a36f1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 12:17:32 +0200 Subject: [PATCH 060/108] clients/upsclient.c: upscli_find_host_cert(), upscli_add_host_cert(): avoid "s_host" varname which confuses WIN32, illumos and other builds [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 0697d7bf25..0a28735cd7 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1287,28 +1287,28 @@ static uint16_t get_port_from_string(const char *str_port) void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl) { /* Support parsing apart authconf section names */ - const char *s_port = strchr(hostname, ':'), *s_host = strchr(hostname, '@'); + const char *substr_port = strchr(hostname, ':'), *substr_host = strchr(hostname, '@'); uint16_t port = NUT_PORT; char host[LARGEBUF]; - if (s_host) { - s_host++; + if (substr_host) { + substr_host++; } else { - s_host = hostname; + substr_host = hostname; } - if (s_port) { + if (substr_port) { snprintf(host, - MIN(sizeof(host) - 1, (size_t)(s_port - s_host + 1)), - "%s", s_host); + MIN(sizeof(host) - 1, (size_t)(substr_port - substr_host + 1)), + "%s", substr_host); - if (s_port[1]) { - port = get_port_from_string(s_port + 1); + 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, s_port + 1); + __func__, hostname, substr_port + 1); port = NUT_PORT; } } @@ -1318,11 +1318,11 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve upsdebugx(4, "%s: split '%s' into hostname '%s' port '%u'", __func__, hostname, - s_port ? host : s_host, + substr_port ? host : substr_host, (unsigned int)port); upscli_add_host_port_cert( - s_port ? host : s_host, + substr_port ? host : substr_host, port, certname, certverify, forcessl); } @@ -1382,29 +1382,29 @@ static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t po #if 0 static HOST_CERT_t* upscli_find_host_cert(const char* hostname) { - const char *s_port = strchr(hostname, ':'); + const char *substr_port = strchr(hostname, ':'); uint16_t port = NUT_PORT; char host[LARGEBUF]; - if (s_port) { + if (substr_port) { snprintf(host, - MIN(sizeof(host) - 1, (size_t)(s_port - hostname)), + MIN(sizeof(host) - 1, (size_t)(substr_port - hostname)), "%s", hostname); - if (s_port[1]) { - port = get_port_from_string(s_port + 1); + 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, s_port + 1); + __func__, hostname, substr_port + 1); port = NUT_PORT; } } } return upscli_find_host_port_cert( - s_port ? host : hostname, + substr_port ? host : hostname, port); } #endif From 46a8e7eb6bd45aced8327189ea9505d7cc177fdb Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 12:38:33 +0200 Subject: [PATCH 061/108] include/strcasestr-static.h, clients/authconf.c, etc.: promote code from drivers/libusb0.c to be a bit more shared [#3329] Follows up from commits b91e34edc79 and f951dce8deb Signed-off-by: Jim Klimov --- clients/authconf.c | 2 + drivers/libusb0.c | 42 +-------------------- include/Makefile.am | 2 +- include/strcasestr-static.h | 73 +++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 include/strcasestr-static.h diff --git a/clients/authconf.c b/clients/authconf.c index 684d643da2..7775eadf88 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -37,6 +37,8 @@ # 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 */ 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/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 */ From bad8533a163bfa5fe9a29186a2b7c89f9f339ba2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 17:52:45 +0200 Subject: [PATCH 062/108] clients/upsclient.c, docs/man/upscli_add_host_cert.txt: refactor upscli_find_host_port_cert() with a "verbose" option, and extend upscli_add_host_port_cert() with check for existing entries [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 37 ++++++++++++++++++++----------- docs/man/upscli_add_host_cert.txt | 4 +++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 0a28735cd7..96e6c9eba9 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -167,7 +167,7 @@ typedef struct HOST_CERT_s { #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); +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; @@ -334,7 +334,7 @@ static SECStatus AuthCertificate(CERTCertDBHandle *arg, PRFileDesc *fd, if (peer && ups) { HOST_CERT_t *cert; - cert = upscli_find_host_port_cert(ups->host, ups->port); + 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); @@ -428,7 +428,7 @@ static SECStatus BadCertHandler(UPSCONN_t *arg, PRFileDesc *fd) * If the certificate verification (user conf) is mandatory, reject authentication * else accept it. */ - cert = arg ? upscli_find_host_port_cert(arg->host, arg->port) : NULL; + cert = arg ? upscli_find_host_port_cert(arg->host, arg->port, 1) : NULL; if (cert != NULL) { return cert->certverify==0 ? SECSuccess : SECFailure; } else { @@ -1329,7 +1329,15 @@ void upscli_add_host_cert(const char* hostname, const char* certname, int certve void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* certname, int certverify, int forcessl) { #if defined(WITH_OPENSSL) || defined(WITH_NSS) - HOST_CERT_t* cert = (HOST_CERT_t *)xmalloc(sizeof(HOST_CERT_t)); + HOST_CERT_t* cert = upscli_find_host_port_cert(hostname, port, 0); + + if (cert) { + upsdebugx(5, "%s: SKIP: found existing CERTHOST with same data (host '%s' and port '%u')", + __func__, hostname, (unsigned int)port); + return; + } + + cert = (HOST_CERT_t *)xmalloc(sizeof(HOST_CERT_t)); upsdebugx(1, "%s: adding CERTHOST: host '%s' port '%u' certname '%s' certverify %d forcessl %d", __func__, hostname, (unsigned int)port, certname, certverify, forcessl); @@ -1352,7 +1360,7 @@ void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* #endif /* WITH_NSS */ } -static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t port) +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; @@ -1362,25 +1370,28 @@ static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t po && strcmp(cert->host, hostname) == 0 && cert->port == port ) { - upsdebugx(4, "%s: found '%s' for '%s':'%u'", - __func__, NUT_STRARG(cert->certname), hostname, (unsigned int)port); + if (verbose) + upsdebugx(4, "%s: found '%s' for '%s':'%u'", + __func__, NUT_STRARG(cert->certname), hostname, (unsigned int)port); return cert; } cert = cert->next; } } - upsdebugx(4, "%s: nothing found for '%s':'%u'", __func__, hostname, (unsigned int)port); + 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) +static HOST_CERT_t* upscli_find_host_cert(const char* hostname, int verbose) { const char *substr_port = strchr(hostname, ':'); uint16_t port = NUT_PORT; @@ -1405,7 +1416,7 @@ static HOST_CERT_t* upscli_find_host_cert(const char* hostname) return upscli_find_host_port_cert( substr_port ? host : hostname, - port); + port, verbose); } #endif @@ -1971,7 +1982,7 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) } { /* scoping */ - HOST_CERT_t *cert = upscli_find_host_port_cert(ups->host, ups->port); + 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 @@ -2445,7 +2456,7 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags ups->port = port; - hostcert = upscli_find_host_port_cert(host, port); + hostcert = upscli_find_host_port_cert(host, port, 1); if (hostcert != NULL) { /* An host security rule is specified. */ diff --git a/docs/man/upscli_add_host_cert.txt b/docs/man/upscli_add_host_cert.txt index 0847b028ce..dcdf49d5d5 100644 --- a/docs/man/upscli_add_host_cert.txt +++ b/docs/man/upscli_add_host_cert.txt @@ -21,7 +21,7 @@ SYNOPSIS void upscli_add_host_port_cert( const char* hostname, - uint16_t port, = + uint16_t port, const char* certname, int certverify, int forcessl) @@ -34,6 +34,8 @@ The *upscli_add_host_cert()* function registers a security rule associated 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 From 0455217efb3157790c282d75fcd56f541361687c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 18:05:59 +0200 Subject: [PATCH 063/108] clients/upsclient.c: upscli_cleanup(): call upscli_free_authconf_list() [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/upsclient.c b/clients/upsclient.c index 96e6c9eba9..f3b411e319 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1444,6 +1444,7 @@ int upscli_cleanup(void) PL_ArenaFinish(); #endif /* WITH_NSS */ + upscli_free_authconf_list(); upscli_initialized = 0; return 1; } From 571d3a2039bd55f69e7303a31353d5e87e7a6c79 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 19 May 2026 18:06:59 +0200 Subject: [PATCH 064/108] clients/{upscmd,upsrw,upslog,upsstats,upsset,upsimage}.c, docs/man/*.txt: add support for nutauth.conf [#3329] Signed-off-by: Jim Klimov --- NEWS.adoc | 18 ++++++++++-- clients/upsc.c | 1 + clients/upscmd.c | 45 +++++++++++++++++++++++++++-- clients/upsimage.c | 22 ++++++++++++-- clients/upslog.c | 60 ++++++++++++++++++++++++++++++++++++--- clients/upsrw.c | 53 ++++++++++++++++++++++++++++++++-- clients/upsset.c | 24 ++++++++++++++-- clients/upsstats.c | 36 ++++++++++++++++++++++- docs/man/upsc.txt | 1 + docs/man/upscmd.txt | 24 +++++++++++++++- docs/man/upsimage.cgi.txt | 5 +++- docs/man/upslog.txt | 29 ++++++++++++++++++- docs/man/upsrw.txt | 24 +++++++++++++++- docs/man/upsset.cgi.txt | 5 +++- docs/man/upsset.conf.txt | 3 +- docs/man/upsstats.cgi.txt | 5 +++- 16 files changed, 329 insertions(+), 26 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index d519aaff0d..1aad919870 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -129,14 +129,28 @@ https://github.com/networkupstools/nut/milestone/13 * Introduced support for "authconf" files to store and convey NUT client authentication details. [issue #3329] - - `upsc` client updates: + - `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 or to not even try reading one. [issues #3329, #3411] + such a file (`default`) or to not even try reading one (`none`). + [issues #3329, #3411] + + - `upslog` client/tool updates: + * Added support for best-effort use of `nutauth.conf` files from default + locations or via `-A` option, as described above. Since this client + can establish multiple connections, keep in mind that currently it + can only identify itself with some one (first seen) client certificate, + if `CERTIDENT` settings are used. Multiple `CERTHOST` directives for + specially trusted servers can be used. [#3329] + + - `upsstats`, `upsset`, `upsimage` CGI client updates: + * Added support for best-effort use of `nutauth.conf` files from default + locations described above (no way to choose the location, other than + by web-server environment variables for CGI calls). [#3329] - `upsmon` client updates: * Introduced support for `CERTFILE` option, so the client can identify diff --git a/clients/upsc.c b/clients/upsc.c index da235a3d0d..127d89c8f4 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -557,6 +557,7 @@ int main(int argc, char **argv) flags_ssl |= UPSCLI_CONN_CERTVERIF; } if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } } diff --git a/clients/upscmd.c b/clients/upscmd.c index 5c14fb58e7..f3d48f8875 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -54,7 +54,7 @@ struct list_t { }; /* For getopt loops; should match usage documented below: */ -static const char optstring[] = "+Dlhu:p:t:wVW:"; +static const char optstring[] = "+Dlhu:p:t:wVW:A:"; static void help(const char *prog) { @@ -79,6 +79,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -D - raise debugging level\n"); printf(" -h - display this help text\n"); @@ -323,7 +326,8 @@ int main(int argc, char **argv) uint16_t port; ssize_t ret; int have_un = 0, have_pw = 0, cmdlist = 0; - char buf[SMALLBUF * 2], username[SMALLBUF], password[SMALLBUF]; + char buf[SMALLBUF * 2], username[SMALLBUF], password[SMALLBUF], *nutauth = NULL, str_port[16]; + int flags_ssl = UPSCLI_CONN_TRYSSL; const char *prog = getprogname_argv0_default(argc > 0 ? argv[0] : NULL, "upscmd"); const char *net_connect_timeout = NULL; @@ -375,6 +379,11 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 'l': cmdlist = 1; break; @@ -416,6 +425,23 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + upscli_read_authconf_file(NULL, 1); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + upscli_read_authconf_file(nutauth, 1); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { fatalx(EXIT_FAILURE, "Error: invalid network timeout: %s", net_connect_timeout); @@ -439,9 +465,22 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } diff --git a/clients/upsimage.c b/clients/upsimage.c index f9adbe8890..138469e837 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -626,8 +626,8 @@ static void clean_exit(void) int main(int argc, char **argv) { - char str[SMALLBUF], *s; - int i, min, nom, max; + char str[SMALLBUF], *s, str_port[16]; + int flags_ssl = UPSCLI_CONN_TRYSSL, i, min, nom, max; double var = 0; #ifdef WIN32 @@ -680,9 +680,25 @@ int main(int argc, char **argv) extractcgiargs(); + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + /* no 'host=' or 'display=' given */ if ((!monhost) || (!cmd)) noimage("No host or display"); @@ -699,7 +715,7 @@ int main(int argc, char **argv) #endif } - if (upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(&ups, hostname, port, flags_ssl) < 0) { noimage("Can't connect to server:\n%s\n", upscli_strerror(&ups)); #ifndef HAVE___ATTRIBUTE__NORETURN diff --git a/clients/upslog.c b/clients/upslog.c index 80977a0179..b5733f2a4d 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -195,7 +195,7 @@ static void help(const char *prog) __attribute__((noreturn)); /* For getopt loops; should match usage documented below: */ -static const char optstring[] = "+hDs:l:i:d:Nf:u:Vp:FBm:W:"; +static const char optstring[] = "+hDs:l:i:d:Nf:u:Vp:FBm:W:A:"; static void help(const char *prog) { @@ -233,6 +233,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -h - display this help text\n"); printf("\n"); printf("Some valid format string escapes:\n"); @@ -526,6 +529,9 @@ int main(int argc, char **argv) const char *net_connect_timeout = NULL; time_t now, nextpoll = 0; const char *user = NULL; + char *nutauth = NULL, str_port[16]; + upscli_authconf_t *ac_default = NULL; + int flags_ssl = UPSCLI_CONN_TRYSSL; struct passwd *new_uid = NULL; const char *pidfilebase = prog; /* For legacy single-ups -s/-l args: */ @@ -582,6 +588,11 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 'h': help(prog); #ifndef HAVE___ATTRIBUTE__NORETURN @@ -621,6 +632,7 @@ int main(int argc, char **argv) free(s); } /* var scope */ break; + case 's': monhost = optarg; break; @@ -705,6 +717,23 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + upscli_read_authconf_file(NULL, 1); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + upscli_read_authconf_file(nutauth, 1); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { fatalx(EXIT_FAILURE, "Error: invalid network timeout: %s", net_connect_timeout); @@ -797,12 +826,15 @@ int main(int argc, char **argv) if (!monhost_len) fatalx(EXIT_FAILURE, "No UPS defined for monitoring - use -s -l , or use -m ; consider -m '*,-' to view updates of all known local devices"); + ac_default = upscli_find_authconf_item(NULL, NULL, NULL); /* Split the system specs in a common fashion for tuples and legacy args */ for ( monhost_ups_current = monhost_ups_anchor, monhost_ups_prev = NULL; monhost_ups_current != NULL; monhost_ups_current = monhost_ups_current->next ) { + upscli_authconf_t *ac_current; + if (upscli_splitname(monhost_ups_current->monhost, &(monhost_ups_current->upsname), &(monhost_ups_current->hostname), &(monhost_ups_current->port)) != 0) { fatalx(EXIT_FAILURE, "Error: invalid UPS definition. Required format: upsname[@hostname[:port]]\n"); } @@ -814,6 +846,26 @@ int main(int argc, char **argv) monhost_ups_current->port ); + /* FIXME: Currently libupsclient allows for one SSL context shared + * by all connections, specifically the CERTIDENT of the client. + * We can have multiple CERTHOST certificates (and/or reading + * users/passwords) though. */ + ac_current = upscli_get_authconf_item(NULL, monhost_ups_current->hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, monhost_ups_current->port) > 0 ? str_port : NULL, 1); + /* Always call this, to register possible CERTHOSTs etc. */ + if (upscli_init_authconf(ac_current) > 0) { + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + // Do not call on the next loop cycle, if any + ac_default = NULL; + } + } + /* Revise the list if some UPS name was an asterisk * (query the data server) */ if (!strcmp(monhost_ups_current->upsname, "*")) { @@ -834,7 +886,7 @@ int main(int argc, char **argv) conn = (UPSCONN_t *)xmalloc(sizeof(*conn)); - if (upscli_connect(conn, monhost_ups_current->hostname, monhost_ups_current->port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(conn, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(conn)); } @@ -954,7 +1006,7 @@ int main(int argc, char **argv) monhost_ups_current->ups = (UPSCONN_t *)xmalloc(sizeof(UPSCONN_t)); - if (upscli_connect(monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, UPSCLI_CONN_TRYSSL) < 0) + if (upscli_connect(monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, flags_ssl) < 0) fprintf(stderr, "Warning: initial connect failed: %s\n", upscli_strerror(monhost_ups_current->ups)); @@ -1039,7 +1091,7 @@ int main(int argc, char **argv) monhost_ups_current->ups, monhost_ups_current->hostname, monhost_ups_current->port, - UPSCLI_CONN_TRYSSL); + flags_ssl); } run_flist(monhost_ups_current); diff --git a/clients/upsrw.c b/clients/upsrw.c index 636d868193..6a8ece2b4d 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -54,7 +54,7 @@ struct list_t { }; /* For getopt loops; should match usage documented below: */ -static const char optstring[] = "+Dhls:p:t:u:wVW:"; +static const char optstring[] = "+Dhls:p:t:u:wVW:A:"; static void help(const char *prog) { @@ -78,6 +78,9 @@ static void help(const char *prog) printf(" -V - display the version of this software\n"); printf(" -W - network timeout for initial connections (default: %s)\n", UPSCLI_DEFAULT_CONNECT_TIMEOUT); + printf(" -A - require use of specified authentication configuration file\n"); + printf(" (pass 'default' to require finding one user- or system-provided\n"); + printf(" locations, or 'none' to not seek any such file)\n"); printf(" -D - raise debugging level\n"); printf(" -h - display this help text\n"); printf("\n"); @@ -683,7 +686,8 @@ int main(int argc, char **argv) uint16_t port; const char *prog = getprogname_argv0_default(argc > 0 ? argv[0] : NULL, "upsrw"); const char *net_connect_timeout = NULL; - char *password = NULL, *username = NULL, *setvar = NULL; + char *password = NULL, *username = NULL, *setvar = NULL, *nutauth = NULL, str_port[16]; + int flags_ssl = UPSCLI_CONN_TRYSSL; NUT_UNUSED_VARIABLE(upslog_start_tmp); upscli_upslog_setprocname(xstrdup(getmyprocname()), nut_common_cookie()); @@ -733,37 +737,50 @@ int main(int argc, char **argv) switch (opt_ret) { case 'D': break; /* See nut_debug_level handled above */ + + case 'A': + nutauth = optarg; + break; + case 's': setvar = optarg; break; + case 'l': if (setvar) { upslogx(LOG_WARNING, "Listing mode requested, overriding setvar specified earlier!"); setvar = NULL; } break; + case 'p': password = optarg; break; + case 't': if (!str_to_uint(optarg, &timeout, 10)) fatal_with_errno(EXIT_FAILURE, "Could not convert the provided value for timeout ('-t' option) to unsigned int"); break; + case 'u': username = optarg; break; + case 'w': tracking_enabled = 1; break; + case 'V': /* just show the version and optional * CONFIG_FLAGS banner if available */ print_banner_once(prog, 1); nut_report_config_flags(); exit(EXIT_SUCCESS); + case 'W': net_connect_timeout = optarg; break; + case 'h': default: help(prog); @@ -771,6 +788,23 @@ int main(int argc, char **argv) } } + if (nutauth) { + if (!strcmp(nutauth, "none")) { + upsdebugx(1, "Using nutauth='%s': skipping auth config", nutauth); + } else { + if (!strcmp(nutauth, "default")) { + upsdebugx(1, "Using nutauth='%s': require a user or system provided file", nutauth); + upscli_read_authconf_file(NULL, 1); + } else { + upsdebugx(1, "Using nutauth='%s': require this file", nutauth); + upscli_read_authconf_file(nutauth, 1); + } + } + } else { + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + } + if (upscli_init_default_connect_timeout(net_connect_timeout, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT) < 0) { fatalx(EXIT_FAILURE, "Error: invalid network timeout: %s", net_connect_timeout); @@ -794,9 +828,22 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); - if (upscli_connect(ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } diff --git a/clients/upsset.c b/clients/upsset.c index 20f70bf8b6..d446c3a984 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -52,6 +52,8 @@ struct list_t { /* network timeout for initial connection, in seconds */ #define UPSCLI_DEFAULT_CONNECT_TIMEOUT "10" +static int flags_ssl = UPSCLI_CONN_TRYSSL; + static char *monups, *username, *password, *function, *upscommand; /* set once the MAGIC_ENABLE_STRING is found in the upsset.conf */ @@ -371,7 +373,7 @@ static void upsd_connect(void) /* NOTREACHED */ } - if (upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) { + if (upscli_connect(&ups, hostname, port, flags_ssl) < 0) { error_page("showsettings", "Connect failure", "Unable to connect to %s: %s", monups, upscli_strerror(&ups)); @@ -1124,8 +1126,8 @@ static void clean_exit(void) int main(int argc, char **argv) { - char *s; - int i; + char *s, str_port[16]; + int i; #ifdef WIN32 /* Required ritual before calling any socket functions */ @@ -1183,11 +1185,27 @@ int main(int argc, char **argv) /* see if the magic string is present in the config file */ check_conf(); + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); extractpostargs(); + if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + /* Nothing POSTed (or parsed correctly)? */ if ((!username) || (!password) || (!function)) loginscreen(); diff --git a/clients/upsstats.c b/clients/upsstats.c index 7d4450970a..a91634a60b 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,7 +540,30 @@ 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) { + if (ac_default->certverify) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + // Do not call on the next loop cycle, if any + ac_default = NULL; + } + } + + if (currups && upscli_connect(&ups, hostname, port, flags_ssl) < 0) fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", currups ? NUT_STRARG(currups->sys) : "", upscli_strerror(&ups)); @@ -1750,9 +1778,15 @@ int main(int argc, char **argv) extractcgiargs(); + upsdebugx(1, "Using best-effort auth config detection"); + upscli_read_authconf_file(NULL, 0); + upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); + /* Prepare for handling in first loop through ups_connect() */ + ac_default = upscli_find_authconf_item(NULL, NULL, NULL); + /* * If json is in the query, bypass all HTML and call display_json() */ diff --git a/docs/man/upsc.txt b/docs/man/upsc.txt index 2d8beed448..f26ee0b3bd 100644 --- a/docs/man/upsc.txt +++ b/docs/man/upsc.txt @@ -183,6 +183,7 @@ SEE ALSO linkman:upslog[8], linkman:ups.conf[5], +linkman:nutauth.conf[5], linkman:upsd[8] Internet resources: diff --git a/docs/man/upscmd.txt b/docs/man/upscmd.txt index d8793248e0..9bfc10b597 100644 --- a/docs/man/upscmd.txt +++ b/docs/man/upscmd.txt @@ -80,6 +80,27 @@ Set the timeout for initial network connections (by default they are indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + UNATTENDED MODE --------------- @@ -132,7 +153,8 @@ It involves magic cookies. SEE ALSO -------- -linkman:upsd[8], linkman:upsrw[8] +linkman:upsd[8], linkman:upsrw[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsimage.cgi.txt b/docs/man/upsimage.cgi.txt index dc38aeb243..e0b762c880 100644 --- a/docs/man/upsimage.cgi.txt +++ b/docs/man/upsimage.cgi.txt @@ -35,6 +35,8 @@ upsstats will only talk to linkman:upsd[8] servers that have been defined in your linkman:hosts.conf[5]. If it complains about "Access to that host is not authorized", check that file first. +SSL access may be further managed by linkman:nutauth.conf[5] file. + FILES ----- @@ -43,7 +45,8 @@ linkman:hosts.conf[5] SEE ALSO -------- -linkman:upsd[8], linkman:upsstats.cgi[8] +linkman:upsd[8], linkman:upsstats.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upslog.txt b/docs/man/upslog.txt index a1499134c9..840970bcee 100644 --- a/docs/man/upslog.txt +++ b/docs/man/upslog.txt @@ -155,6 +155,27 @@ Set the timeout for initial network connections (by default they are indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + SERVICE DELAYS -------------- @@ -204,6 +225,11 @@ Since NUT v2.8.3, the single-UPS options are added to the list of tuples, so both legacy and new options can be reliably used to monitor multiple devices in the same run. +Since this client can establish multiple connections, keep in mind that +currently it can only identify itself with some one (first seen) client +certificate, if `CERTIDENT` settings are used in the linkman:nutauth.conf[5] +file. Multiple `CERTHOST` directives for specially trusted servers can be used. + SEE ALSO -------- @@ -216,7 +242,8 @@ Clients: ~~~~~~~~ linkman:upsc[8], linkman:upscmd[8], -linkman:upsrw[8], linkman:upsmon[8], linkman:upssched[8] +linkman:upsrw[8], linkman:upsmon[8], linkman:upssched[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsrw.txt b/docs/man/upsrw.txt index fdb4891cfd..4578b09037 100644 --- a/docs/man/upsrw.txt +++ b/docs/man/upsrw.txt @@ -97,6 +97,27 @@ Set the timeout for initial network connections (by default they are indefinitely non-blocking, or until the system interrupts the attempt). Overrides the optional `NUT_DEFAULT_CONNECT_TIMEOUT` environment variable. +*-A* '/path/to/nutauth.conf':: + + Require use of the specified linkman:nutauth.conf[5] file (fail if absent, + not accessible, or has content errors when parsed). ++ +By silent default, the client tries best-effort (non-fatal) detection of a + configuration file in per-user locations `${HOME}/.config/nut/nutauth.conf` + or `${HOME}/.nutauth.conf`, or site default `${NUT_CONFPATH}/nutauth.conf`. ++ +NOTE: If the `NUT_AUTHCONF_FILE` environment variable is exported, only that + exact full (relative or absolute) file path and name would be tried as the + default; otherwise, if the `NUT_AUTHCONF_PATH` environment variable is + exported, only a `nutauth.conf` file in that directory would be tried as the + default (instead of looking at hard-coded default locations listed above). ++ +Special values: + +* `default`: require a default file in one of the locations found per rules + listed above; +* `none`: do not even try to find and load any such file (legacy default). + UNATTENDED MODE --------------- @@ -144,7 +165,8 @@ confusing. SEE ALSO -------- -linkman:upsd[8], linkman:upscmd[8] +linkman:upsd[8], linkman:upscmd[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsset.cgi.txt b/docs/man/upsset.cgi.txt index bb88d81b9b..9380ee7b31 100644 --- a/docs/man/upsset.cgi.txt +++ b/docs/man/upsset.cgi.txt @@ -82,6 +82,8 @@ upsset will only talk to linkman:upsd[8] servers that have been defined in your linkman:hosts.conf[5]. If it complains about "Access to that host is not authorized", check your hosts.conf first. +SSL access may be further managed by linkman:nutauth.conf[5] file. + SECURITY -------- @@ -95,7 +97,8 @@ The short explanation is--if you can't lock it down, don't try to run it. FILES ----- -linkman:hosts.conf[5], linkman:upsset.conf[5] +linkman:hosts.conf[5], linkman:upsset.conf[5], +linkman:nutauth.conf[5] SEE ALSO -------- diff --git a/docs/man/upsset.conf.txt b/docs/man/upsset.conf.txt index b617bb0df3..bf9c3200f1 100644 --- a/docs/man/upsset.conf.txt +++ b/docs/man/upsset.conf.txt @@ -196,7 +196,8 @@ web server, don't blame me. SEE ALSO -------- -linkman:upsset.cgi[8] +linkman:upsset.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index b30a37a03e..6d76a1ee1a 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -41,6 +41,8 @@ upsstats will only talk to linkman:upsd[8] servers that have been defined in your linkman:hosts.conf[5]. If it complains that "Access to that host is not authorized", check that file first. +SSL access may be further managed by linkman:nutauth.conf[5] file. + TEMPLATES --------- @@ -114,7 +116,8 @@ linkman:hosts.conf[5], linkman:upsstats.html[5], upsstats-single.html SEE ALSO -------- -linkman:upsimage.cgi[8] +linkman:upsimage.cgi[8], +linkman:nutauth.conf[5] Internet resources: ~~~~~~~~~~~~~~~~~~~ From 0f5a28191514252de6ba8faad9e3e4fa0ce5549a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 21 May 2026 14:07:47 +0200 Subject: [PATCH 065/108] tests/Makefile.am: test_authconf: consider LIBSSL flags [#3329, #1711] Builds with NSS tend to fail on some platforms due to not locating libnss3.so (which is hidden from common searches in an nss/mps subdirectory) Signed-off-by: Jim Klimov --- tests/Makefile.am | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Makefile.am b/tests/Makefile.am index 2233d9fe4c..bfda0cff97 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -120,7 +120,14 @@ nutbooltest_SOURCES = nutbooltest.c 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 From ad86df664ab67cd32da93ce3eab121d7deb0c0bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 22 May 2026 09:15:15 +0200 Subject: [PATCH 066/108] clients/authconf.{c,h}, docs/man/upscli_dump_authconf_item.txt: do not specify "FILE *restrict stream", some platforms think "restrict" is the variable name [#3329] Signed-off-by: Jim Klimov --- clients/authconf.c | 8 ++++---- clients/authconf.h | 4 ++-- docs/man/upscli_dump_authconf_item.txt | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index 7775eadf88..aff088a44d 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -245,7 +245,7 @@ upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node) return NULL; } -static int upscli_dump_authconf_line_str(FILE *restrict stream, const char *var, const char *val, const char *indent, int for_debug) +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; @@ -278,7 +278,7 @@ static int upscli_dump_authconf_line_str(FILE *restrict stream, const char *var, return res; } -static int upscli_dump_authconf_line_int(FILE *restrict stream, const char *var, int val, const char *indent, int for_debug) +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; @@ -298,7 +298,7 @@ static int upscli_dump_authconf_line_int(FILE *restrict stream, const char *var, return res; } -int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, int for_debug, int show_pass) +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; @@ -380,7 +380,7 @@ int upscli_dump_authconf_item(FILE *restrict stream, upscli_authconf_t *node, in return ret; } -size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug, int show_pass) +size_t upscli_dump_authconf_list(FILE *stream, int for_debug, int show_pass) { upscli_authconf_t *node = authconf_list; size_t count = 0; diff --git a/clients/authconf.h b/clients/authconf.h index 8abf507827..dfbcf55123 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -111,11 +111,11 @@ upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, * 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 *restrict stream, upscli_authconf_t *node, int for_debug, int show_pass); +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 *restrict stream, int for_debug, int show_pass); +size_t upscli_dump_authconf_list(FILE *stream, int for_debug, int show_pass); #ifdef __cplusplus } diff --git a/docs/man/upscli_dump_authconf_item.txt b/docs/man/upscli_dump_authconf_item.txt index 176b02f70d..91129905db 100644 --- a/docs/man/upscli_dump_authconf_item.txt +++ b/docs/man/upscli_dump_authconf_item.txt @@ -12,10 +12,10 @@ SYNOPSIS ------ #include - int upscli_dump_authconf_item(FILE *restrict stream, + int upscli_dump_authconf_item(FILE *stream, upscli_authconf_t *node, int for_debug, int show_pass); - size_t upscli_dump_authconf_list(FILE *restrict stream, + size_t upscli_dump_authconf_list(FILE *stream, int for_debug, int show_pass); ------ From 310c41eba6704a15320b7dbcb67af233c90f0365 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 25 May 2026 11:21:27 +0000 Subject: [PATCH 067/108] server/netssl.c: ssl_init(): abort with fatalx() and better explanations when SSL setup was requested but failed [#3331] Signed-off-by: Jim Klimov --- NEWS.adoc | 3 ++ UPGRADING.adoc | 5 +++ docs/man/upsd.conf.txt | 9 +++++ server/netssl.c | 82 ++++++++++++++++++++++++++---------------- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 1aad919870..ba15c41161 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -175,6 +175,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 503986cfec..fe3624d3ff 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -46,6 +46,11 @@ 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 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/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 */ } From 589d3d83586952e6eb3e5f3b38b91cdcfcecca06 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 6 Jun 2026 19:37:37 +0200 Subject: [PATCH 068/108] clients/upsclient.{c,h}, docs/man, upscmd.c, upsrw.c, upsmon.c: refactor with upscli_authenticate() to unite different tricks [#3329] Introduce upscli_authenticate_authconf() for completeness. Signed-off-by: Jim Klimov --- clients/upsclient.c | 139 ++++++++++++++++++++++++++++++- clients/upsclient.h | 34 ++++++++ clients/upscmd.c | 79 +++--------------- clients/upsmon.c | 38 +-------- clients/upsrw.c | 88 +++++-------------- docs/man/Makefile.am | 11 +++ docs/man/upsc.txt | 11 ++- docs/man/upscli_authenticate.txt | 57 +++++++++++++ docs/man/upscmd.txt | 16 +++- docs/man/upslog.txt | 5 +- docs/man/upsrw.txt | 23 +++-- 11 files changed, 310 insertions(+), 191 deletions(-) create mode 100644 docs/man/upscli_authenticate.txt diff --git a/clients/upsclient.c b/clients/upsclient.c index f3b411e319..13a683fb8e 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 @@ -2580,7 +2582,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; } @@ -3210,6 +3213,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[SMALLBUF], pass[SMALLBUF]; + 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 4f47c9151b..28bd76960d 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -198,6 +198,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); diff --git a/clients/upscmd.c b/clients/upscmd.c index f3d48f8875..269cc8403f 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -24,7 +24,6 @@ #include "nut_platform.h" #ifndef WIN32 -#include #include #include #include @@ -324,7 +323,7 @@ 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], *nutauth = NULL, str_port[16]; int flags_ssl = UPSCLI_CONN_TRYSSL; @@ -465,7 +464,8 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ - if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + 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); if (ac_default) { if (ac_default->certverify) { @@ -499,73 +499,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 (!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)); + 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)); } - - 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/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 6a8ece2b4d..91a1099bf2 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -24,7 +24,6 @@ #include "nut_platform.h" #ifndef WIN32 -#include #include #include #include @@ -230,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) { @@ -286,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]])"); @@ -684,6 +619,7 @@ 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, *nutauth = NULL, str_port[16]; @@ -828,7 +764,8 @@ int main(int argc, char **argv) } setproctag(argv[0]); /* ups[@host[:port]] */ - if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + 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); if (ac_default) { if (ac_default->certverify) { @@ -849,7 +786,20 @@ int main(int argc, char **argv) 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/docs/man/Makefile.am b/docs/man/Makefile.am index ea70ae9013..75e4449ce9 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -535,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 \ @@ -731,6 +732,8 @@ 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) \ @@ -820,6 +823,10 @@ UPSCLI_INIT_DEPS = upscli_init2.$(MAN_SECTION_API) upscli_init_authconf.$(MAN_SE $(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 $@ @@ -913,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 \ @@ -998,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 $? $@ diff --git a/docs/man/upsc.txt b/docs/man/upsc.txt index f26ee0b3bd..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 ----------- @@ -92,7 +92,10 @@ COMMON OPTIONS *-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). + 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` 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/upscmd.txt b/docs/man/upscmd.txt index 9bfc10b597..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 @@ -83,7 +90,8 @@ 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). + 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` diff --git a/docs/man/upslog.txt b/docs/man/upslog.txt index 840970bcee..97f9223339 100644 --- a/docs/man/upslog.txt +++ b/docs/man/upslog.txt @@ -158,7 +158,10 @@ 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). + 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` diff --git a/docs/man/upsrw.txt b/docs/man/upsrw.txt index 4578b09037..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 @@ -100,7 +106,8 @@ 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). + 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` From f099775e1ef87940813203beb6a7a7818d4c861e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 6 Jun 2026 21:40:02 +0200 Subject: [PATCH 069/108] clients/upsc.c: support upscli_authenticate_authconf() [#3411, #3329] Signed-off-by: Jim Klimov --- clients/upsc.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/clients/upsc.c b/clients/upsc.c index 127d89c8f4..bb51c484c6 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -408,6 +408,7 @@ 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; @@ -550,7 +551,8 @@ int main(int argc, char **argv) upsdebugx(1, "upsname='%s' hostname='%s' port='%" PRIu16 "'", NUT_STRARG(upsname), NUT_STRARG(hostname), port); - if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + 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); if (ac_default) { if (ac_default->certverify) { @@ -569,6 +571,10 @@ int main(int argc, char **argv) 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); From 9a00dc28e1cf500f14443b1a471e186c375dac77 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 6 Jun 2026 21:42:26 +0200 Subject: [PATCH 070/108] clients/ups*.c: comment in CGI clients about auto-login (not wise for that use case) [#3329, #3411] Signed-off-by: Jim Klimov --- clients/upsimage.c | 12 +++++++++++- clients/upsset.c | 10 ++++++++-- clients/upsstats.c | 12 +++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/clients/upsimage.c b/clients/upsimage.c index 138469e837..bf7a67bc91 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -628,6 +628,7 @@ int main(int argc, char **argv) { 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 @@ -686,7 +687,8 @@ int main(int argc, char **argv) upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); atexit(clean_exit); - if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + 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); if (ac_default) { if (ac_default->certverify) { @@ -723,6 +725,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/upsset.c b/clients/upsset.c index d446c3a984..09f5e78ad2 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1127,6 +1127,7 @@ static void clean_exit(void) int main(int argc, char **argv) { char *s, str_port[16]; + upscli_authconf_t *ac_conn = NULL; int i; #ifdef WIN32 @@ -1193,7 +1194,8 @@ int main(int argc, char **argv) extractpostargs(); - if (upscli_init_authconf(upscli_get_authconf_item(NULL, hostname, snprintf(str_port, sizeof(str_port), "%" PRIu16, port) > 0 ? str_port : NULL, 1)) > 0) { + 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); if (ac_default) { if (ac_default->certverify) { @@ -1206,7 +1208,11 @@ int main(int argc, char **argv) } } - /* Nothing POSTed (or parsed correctly)? */ + /* 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 a91634a60b..81426f58eb 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -548,6 +548,7 @@ static void ups_connect(void) 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) { @@ -563,10 +564,19 @@ static void ups_connect(void) } } - if (currups && upscli_connect(&ups, hostname, port, flags_ssl) < 0) + 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]", From 016179817cf7e2fb1a13561a81db0dd8914938f6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 6 Jun 2026 22:32:10 +0200 Subject: [PATCH 071/108] clients/upsclient.c: upscli_authenticate(): reduce buf sizes for result to fit UPSCLI_NETBUF_LEN [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 13a683fb8e..7406ce3975 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -3216,7 +3216,7 @@ int upscli_upserror(UPSCONN_t *ups) 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[SMALLBUF], pass[SMALLBUF]; + 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; From 6a5df7e7c1af8771a17d73e9d06ae8d478f7afaa Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 21 Jun 2026 18:46:57 +0000 Subject: [PATCH 072/108] dummy-ups: add NUT authconf support [#3329] Signed-off-by: Jim Klimov --- NEWS.adoc | 5 +++ docs/man/dummy-ups.txt | 15 +++++++++ drivers/dummy-ups.c | 70 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index ba15c41161..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 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/drivers/dummy-ups.c b/drivers/dummy-ups.c index e618f89cfd..7da2682d43 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,47 @@ 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) + 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); + if (ac_default) { + if (ac_default->certverify) + flags_ssl |= UPSCLI_CONN_CERTVERIF; + if (ac_default->forcessl) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + } + 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)) { + fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); + } } - } - else - { - upsdebugx(1, "Connected to %s@%s", client_upsname, hostname); } if (upsclient_update_vars() < 0) { @@ -455,6 +479,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 +506,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 From c5ccc7b24d8ae089c34d35b3695866c7829d5d47 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 21 Jun 2026 18:55:41 +0000 Subject: [PATCH 073/108] GitIgnore tests/test_authconf if built [#3329] Signed-off-by: Jim Klimov --- tests/.gitignore | 1 + 1 file changed, 1 insertion(+) 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 From d6cd34e35b0676cf20352f03cd90f3e6f42d677f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 16 Jun 2026 11:10:09 +0200 Subject: [PATCH 074/108] scripts/obs/debian.rules, scripts/obs/nut.spec: update comments about SO_MAJOR_LIB* version hassle Signed-off-by: Jim Klimov --- scripts/obs/debian.rules | 3 ++- scripts/obs/nut.spec | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 72fbb3c0d4..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 From 372329de21c2d348c9ca219d4b6dfef4a3e6a888 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 16 Jun 2026 11:10:59 +0200 Subject: [PATCH 075/108] tools/nut-scanner/*, docs/man/nutscan_scan_nut.txt: introduce nutscan_scan_ip_range_nut_authconf() and nutscan_scan_nut_authconf() with nutscan_nut_authconf_t argument [#3329] Make old methods wrappers of the new ones, so existing libnutscan clients continue working as they were (no breaking ABI change, just new methods). Signed-off-by: Jim Klimov --- docs/man/nutscan_scan_nut.txt | 21 ++++++++++++++++ tools/nut-scanner/Makefile.am | 2 +- tools/nut-scanner/nut-scan.h | 13 ++++++++++ tools/nut-scanner/scan_nut.c | 46 +++++++++++++++++++++++++++++++---- 4 files changed, 76 insertions(+), 6 deletions(-) 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/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..abb0abe06c 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -38,6 +38,10 @@ int nutscan_unload_upsclient_library(void); #define SCAN_NUT_DRIVERNAME "dummy-ups" +/* TODO: Align with what upsc et al do, this being a string (arg/envvar) + * and upscli_init_default_connect_timeout() calling the shots */ +#define UPSCLI_DEFAULT_CONNECT_TIMEOUT 10 + /* dynamic link library stuff */ static lt_dlhandle dl_handle = NULL; static const char *dl_error = NULL; @@ -425,6 +429,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 +452,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 +463,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; @@ -674,12 +710,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,7 +730,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut(nutscan_ip_range_list_t * irl, cons break; } - nut_arg->timeout = usec_timeout; + nut_arg->timeout = sec ? sec->usec_timeout : UPSCLI_DEFAULT_CONNECT_TIMEOUT; nut_arg->hostname = ip_dest; #ifdef HAVE_PTHREAD From 4d95eb13c428a928f4a4597e8422ebb5619a401a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 16 Jun 2026 11:31:20 +0200 Subject: [PATCH 076/108] tools/nut-scanner/scan_nut.c: detect if authconf-related methods are present in libupsclient build we try to use [#3329] Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_nut.c | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index abb0abe06c..41cb29ca01 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -61,6 +61,16 @@ 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 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); /* This variable collects device(s) from a sequential or parallel scan, * is returned to caller, and cleared to allow subsequent independent scans */ @@ -260,6 +270,62 @@ 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_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); + } + /* Passed final lt_dlsym() */ symbol = NULL; From fbccef92a676afcb9df9590d0fa5ba51f2375624 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 17 Jun 2026 14:12:26 +0200 Subject: [PATCH 077/108] tools/nut-scanner/scan_nut.c: nutscan_scan_ip_range_nut_authconf(): detect desired timeout like other NUT clients do [#3329] Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_nut.c | 49 ++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 41cb29ca01..e4d6174034 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -38,9 +38,10 @@ int nutscan_unload_upsclient_library(void); #define SCAN_NUT_DRIVERNAME "dummy-ups" -/* TODO: Align with what upsc et al do, this being a string (arg/envvar) - * and upscli_init_default_connect_timeout() calling the shots */ -#define UPSCLI_DEFAULT_CONNECT_TIMEOUT 10 +/* 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; @@ -559,7 +560,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * #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; @@ -574,6 +575,41 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * size_t max_threads_scantype = max_threads_oldnut; # endif + /* 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); + } + } + +#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) @@ -796,7 +832,10 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * break; } - nut_arg->timeout = sec ? sec->usec_timeout : UPSCLI_DEFAULT_CONNECT_TIMEOUT; + /* 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; #ifdef HAVE_PTHREAD From 93b3b807d80e186e08215e14e5e61a87e6a3002e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 17 Jun 2026 14:12:57 +0200 Subject: [PATCH 078/108] tools/nut-scanner/scan_nut.c: nutscan_scan_ip_range_nut_authconf(): detect and load NUT auth conf file like other clients do [#3329] Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_nut.c | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index e4d6174034..185f9bbee4 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -575,6 +575,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * size_t max_threads_scantype = max_threads_oldnut; # endif + const char *nutauth = sec ? sec->authconf_file : 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. */ @@ -604,6 +605,48 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * } } + if (nutauth && *nutauth && strcmp(nutauth, "none")) { + /* Non-trivial, not a skip */ + if (!( + nut_upscli_authenticate_authconf && + nut_upscli_read_authconf_file && + nut_upscli_init_authconf && + nut_upscli_find_authconf_item && + nut_upscli_get_authconf_item + )) { + 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(); From d3946c56841ce4203380234274729352d2c16f61 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 17 Jun 2026 14:24:37 +0200 Subject: [PATCH 079/108] clients/upsclient.h: update comments for methods about default_connect_timeout [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/clients/upsclient.h b/clients/upsclient.h index 28bd76960d..cf0487022a 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -253,14 +253,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 From 028fc1a544b762b91bd1ff898ee4c74359ddb86c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 19 Jun 2026 16:17:24 +0200 Subject: [PATCH 080/108] clients/upsclient.{c,h}: add pthreads support for first_host_cert-based list manipulation, and methods to remove items from list [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 170 +++++++++++++++++++++++++++++++++++++++++++- clients/upsclient.h | 5 ++ 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 7406ce3975..a63b86e625 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -178,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 */ @@ -187,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; @@ -1344,13 +1382,22 @@ void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* upsdebugx(1, "%s: adding CERTHOST: host '%s' port '%u' certname '%s' certverify %d forcessl %d", __func__, hostname, (unsigned int)port, certname, certverify, forcessl); - cert->next = first_host_cert; cert->host = xstrdup(hostname); cert->port = port ? port : NUT_PORT; cert->certname = xstrdup(certname); cert->certverify = certverify; cert->forcessl = forcessl; + + /* 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); @@ -1367,6 +1414,10 @@ static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t po #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 @@ -1375,10 +1426,17 @@ static HOST_CERT_t* upscli_find_host_port_cert(const char* hostname, uint16_t po 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); @@ -1422,6 +1480,115 @@ static HOST_CERT_t* upscli_find_host_cert(const char* hostname, int 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); +} + +static void upscli_free_host_port_cert_data(HOST_CERT_t* cert) +{ + if (!cert) + return; + + free(cert->host); + free(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; +} + +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 + } +#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 @@ -1446,6 +1613,7 @@ int upscli_cleanup(void) PL_ArenaFinish(); #endif /* WITH_NSS */ + upscli_free_host_cert_list(); upscli_free_authconf_list(); upscli_initialized = 0; return 1; diff --git a/clients/upsclient.h b/clients/upsclient.h index cf0487022a..59460080c8 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -164,6 +164,11 @@ void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* /* 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, From 0d9f21c0a28495a1ba16286e1861790436946e29 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 19 Jun 2026 16:18:47 +0200 Subject: [PATCH 081/108] tools/nut-scanner/scan_nut.c: follow up on NUT authconf support when scanning "old nut" servers [#3329] Signed-off-by: Jim Klimov --- tools/nut-scanner/scan_nut.c | 116 ++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 9 deletions(-) diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 185f9bbee4..2b7c916bda 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -69,9 +69,12 @@ static upscli_authconf_t *(*nut_upscli_get_authconf_item)(const char *user, 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); /* This variable collects device(s) from a sequential or parallel scan, * is returned to caller, and cleared to allow subsequent independent scans */ @@ -92,6 +95,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"; @@ -303,6 +308,14 @@ int nutscan_load_upsclient_library(const char *libname_path) __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) { @@ -327,6 +340,22 @@ int nutscan_load_upsclient_library(const char *libname_path) __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); + } + /* Passed final lt_dlsym() */ symbol = NULL; @@ -393,7 +422,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) { @@ -405,6 +434,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; @@ -576,10 +612,22 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * # 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);; + 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 @@ -607,13 +655,7 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * if (nutauth && *nutauth && strcmp(nutauth, "none")) { /* Non-trivial, not a skip */ - if (!( - nut_upscli_authenticate_authconf && - nut_upscli_read_authconf_file && - nut_upscli_init_authconf && - nut_upscli_find_authconf_item && - nut_upscli_get_authconf_item - )) { + 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; } @@ -739,6 +781,17 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * ip_str = nutscan_ip_ranges_iter_init(&ip, irl); + ac_default = (*nut_upscli_find_authconf_item)(NULL, NULL, NULL); + if (ac_default) { + if (ac_default->certverify > 0) { + flags_ssl |= UPSCLI_CONN_CERTVERIF; + } + if (ac_default->forcessl > 0) { + flags_ssl ^= UPSCLI_CONN_TRYSSL; + flags_ssl |= UPSCLI_CONN_REQSSL; + } + } + while (ip_str != NULL) { #ifdef HAVE_PTHREAD /* NOTE: With many enough targets to scan, this can crash @@ -880,6 +933,41 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * * 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) { + switch (nut_arg->ac_current->certverify) { + case 0: nut_arg->flags_ssl ^= UPSCLI_CONN_CERTVERIF; break; + case 1: nut_arg->flags_ssl |= UPSCLI_CONN_CERTVERIF; break; + case -1: break; + default: break; + } + + switch (nut_arg->ac_current->forcessl) { + case 0: + nut_arg->flags_ssl ^= UPSCLI_CONN_TRYSSL; + nut_arg->flags_ssl ^= UPSCLI_CONN_REQSSL; + break; + case 1: + nut_arg->flags_ssl ^= UPSCLI_CONN_TRYSSL; + nut_arg->flags_ssl |= UPSCLI_CONN_REQSSL; + break; + case -1: break; + default: break; + } + } + } else { + nut_arg->ac_current = NULL; + } #ifdef HAVE_PTHREAD if (pthread_create(&thread, NULL, list_nut_devices_thready, (void*)nut_arg) == 0) { @@ -910,12 +998,22 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * 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 */ From 68ed116bda1ea208ef4b26293f0e965457f8b634 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 01:52:22 +0200 Subject: [PATCH 082/108] clients/*.c: only consider authconf default certverify and flags_ssl if positive [#3329] Signed-off-by: Jim Klimov --- clients/upsc.c | 4 ++-- clients/upscmd.c | 4 ++-- clients/upsimage.c | 4 ++-- clients/upslog.c | 8 ++++---- clients/upsrw.c | 4 ++-- clients/upsset.c | 4 ++-- clients/upsstats.c | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/clients/upsc.c b/clients/upsc.c index bb51c484c6..748286bbf1 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -555,10 +555,10 @@ int main(int argc, char **argv) if (ac_conn && upscli_init_authconf(ac_conn) > 0) { upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); if (ac_default) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } diff --git a/clients/upscmd.c b/clients/upscmd.c index 269cc8403f..9ab27b871b 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -468,10 +468,10 @@ int main(int argc, char **argv) if (ac_conn && upscli_init_authconf(ac_conn) > 0) { upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); if (ac_default) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } diff --git a/clients/upsimage.c b/clients/upsimage.c index bf7a67bc91..e048454eba 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -691,10 +691,10 @@ int main(int argc, char **argv) if (ac_conn && upscli_init_authconf(ac_conn) > 0) { upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); if (ac_default) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } diff --git a/clients/upslog.c b/clients/upslog.c index b5733f2a4d..376ea3f02f 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -846,18 +846,18 @@ int main(int argc, char **argv) monhost_ups_current->port ); - /* FIXME: Currently libupsclient allows for one SSL context shared - * by all connections, specifically the CERTIDENT of the client. + /* 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) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } diff --git a/clients/upsrw.c b/clients/upsrw.c index 91a1099bf2..294c87c09f 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -768,10 +768,10 @@ int main(int argc, char **argv) if (ac_conn && upscli_init_authconf(ac_conn) > 0) { upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); if (ac_default) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } diff --git a/clients/upsset.c b/clients/upsset.c index 09f5e78ad2..ba73fe4acd 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1198,10 +1198,10 @@ int main(int argc, char **argv) if (ac_conn && upscli_init_authconf(ac_conn) > 0) { upscli_authconf_t *ac_default = upscli_find_authconf_item(NULL, NULL, NULL); if (ac_default) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } diff --git a/clients/upsstats.c b/clients/upsstats.c index 81426f58eb..7ed98334db 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -552,10 +552,10 @@ static void ups_connect(void) /* Always call this, to register possible CERTHOSTs etc. */ if (upscli_init_authconf(ac_current) > 0) { if (ac_default) { - if (ac_default->certverify) { + if (ac_default->certverify > 0) { flags_ssl |= UPSCLI_CONN_CERTVERIF; } - if (ac_default->forcessl) { + if (ac_default->forcessl > 0) { flags_ssl ^= UPSCLI_CONN_TRYSSL; flags_ssl |= UPSCLI_CONN_REQSSL; } From cbb28d6a10e844a5c95e4932408512b6a96e23f0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 02:13:47 +0200 Subject: [PATCH 083/108] tools/nut-scanner/scan_nut.c, clients/*.c: refactor with upscli_authconf_update_conn_flags() [#3329] Signed-off-by: Jim Klimov --- clients/upsc.c | 10 +------- clients/upsclient.c | 47 ++++++++++++++++++++++++++++++++++++ clients/upsclient.h | 3 +++ clients/upscmd.c | 10 +------- clients/upsimage.c | 10 +------- clients/upslog.c | 9 ++----- clients/upsrw.c | 10 +------- clients/upsset.c | 10 +------- clients/upsstats.c | 9 ++----- tools/nut-scanner/scan_nut.c | 43 ++++++++++++--------------------- 10 files changed, 74 insertions(+), 87 deletions(-) diff --git a/clients/upsc.c b/clients/upsc.c index 748286bbf1..7fc0d61af6 100644 --- a/clients/upsc.c +++ b/clients/upsc.c @@ -554,15 +554,7 @@ int main(int argc, char **argv) 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); - if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); } ups = (UPSCONN_t *)xmalloc(sizeof(*ups)); diff --git a/clients/upsclient.c b/clients/upsclient.c index a63b86e625..62bc6a95dc 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -877,6 +877,53 @@ static int openssl_cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) #endif +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. diff --git a/clients/upsclient.h b/clients/upsclient.h index 59460080c8..e3d407caee 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -339,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 9ab27b871b..4f1259cf2a 100644 --- a/clients/upscmd.c +++ b/clients/upscmd.c @@ -467,15 +467,7 @@ int main(int argc, char **argv) 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); - if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); } ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); diff --git a/clients/upsimage.c b/clients/upsimage.c index e048454eba..bf7a3dd5bd 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -690,15 +690,7 @@ int main(int argc, char **argv) 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); - if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); } /* no 'host=' or 'display=' given */ diff --git a/clients/upslog.c b/clients/upslog.c index 376ea3f02f..a2a9e88fc8 100644 --- a/clients/upslog.c +++ b/clients/upslog.c @@ -854,13 +854,8 @@ int main(int argc, char **argv) /* Always call this, to register possible CERTHOSTs etc. */ if (upscli_init_authconf(ac_current) > 0) { if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + // Do not call on the next loop cycle, if any ac_default = NULL; } diff --git a/clients/upsrw.c b/clients/upsrw.c index 294c87c09f..fb36eb21fb 100644 --- a/clients/upsrw.c +++ b/clients/upsrw.c @@ -767,15 +767,7 @@ int main(int argc, char **argv) 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); - if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); } ups = (UPSCONN_t *)xcalloc(1, sizeof(*ups)); diff --git a/clients/upsset.c b/clients/upsset.c index ba73fe4acd..0e0ecfb55a 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1197,15 +1197,7 @@ int main(int argc, char **argv) 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); - if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); } /* Nothing POSTed (or parsed correctly)? diff --git a/clients/upsstats.c b/clients/upsstats.c index 7ed98334db..778e726173 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -552,13 +552,8 @@ static void ups_connect(void) /* Always call this, to register possible CERTHOSTs etc. */ if (upscli_init_authconf(ac_current) > 0) { if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); + // Do not call on the next loop cycle, if any ac_default = NULL; } diff --git a/tools/nut-scanner/scan_nut.c b/tools/nut-scanner/scan_nut.c index 2b7c916bda..4700b5387a 100644 --- a/tools/nut-scanner/scan_nut.c +++ b/tools/nut-scanner/scan_nut.c @@ -75,6 +75,7 @@ static int (*nut_upscli_authenticate_authconf)(UPSCONN_t *ups, upscli_authconf_t 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 */ @@ -356,6 +357,14 @@ int nutscan_load_upsclient_library(const char *libname_path) __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; @@ -781,14 +790,10 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * ip_str = nutscan_ip_ranges_iter_init(&ip, irl); - ac_default = (*nut_upscli_find_authconf_item)(NULL, NULL, NULL); - if (ac_default) { - if (ac_default->certverify > 0) { - flags_ssl |= UPSCLI_CONN_CERTVERIF; - } - if (ac_default->forcessl > 0) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; + 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); } } @@ -944,26 +949,8 @@ nutscan_device_t * nutscan_scan_ip_range_nut_authconf(nutscan_ip_range_list_t * * 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) { - switch (nut_arg->ac_current->certverify) { - case 0: nut_arg->flags_ssl ^= UPSCLI_CONN_CERTVERIF; break; - case 1: nut_arg->flags_ssl |= UPSCLI_CONN_CERTVERIF; break; - case -1: break; - default: break; - } - - switch (nut_arg->ac_current->forcessl) { - case 0: - nut_arg->flags_ssl ^= UPSCLI_CONN_TRYSSL; - nut_arg->flags_ssl ^= UPSCLI_CONN_REQSSL; - break; - case 1: - nut_arg->flags_ssl ^= UPSCLI_CONN_TRYSSL; - nut_arg->flags_ssl |= UPSCLI_CONN_REQSSL; - break; - case -1: break; - default: break; - } + 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; From 34af567d77bd8dd19111b8e137e1ede6755a3784 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 02:17:00 +0200 Subject: [PATCH 084/108] drivers/dummy-ups.c: refactor with upscli_authconf_update_conn_flags() [#3329] Signed-off-by: Jim Klimov --- drivers/dummy-ups.c | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/drivers/dummy-ups.c b/drivers/dummy-ups.c index 7da2682d43..86c4ff97e7 100644 --- a/drivers/dummy-ups.c +++ b/drivers/dummy-ups.c @@ -159,7 +159,7 @@ void upsdrv_initinfo(void) } /* Connect to the target */ ups = (UPSCONN_t *)xmalloc(sizeof(*ups)); - { + { /* scoping */ upscli_authconf_t *ac_conn = NULL; int flags_ssl = UPSCLI_CONN_TRYSSL; char str_port[16]; @@ -167,27 +167,20 @@ void upsdrv_initinfo(void) 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); - if (ac_default) { - if (ac_default->certverify) - flags_ssl |= UPSCLI_CONN_CERTVERIF; - if (ac_default->forcessl) { - flags_ssl ^= UPSCLI_CONN_TRYSSL; - flags_ssl |= UPSCLI_CONN_REQSSL; - } - } + upscli_authconf_update_conn_flags(ac_default, &flags_ssl); } if (upscli_connect(ups, hostname, port, flags_ssl) < 0) { - if(repeater_disable_strict_start == 1) + 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)); + "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 @@ -196,7 +189,7 @@ void upsdrv_initinfo(void) } 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)) { + if (upscli_authenticate_authconf(ups, ac_conn) < 0) { fatalx(EXIT_FAILURE, "Error: %s", upscli_strerror(ups)); } } @@ -216,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! */ From 9e560f73622fac683ba78d08aeadb67db60f5b0c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 02:28:27 +0200 Subject: [PATCH 085/108] clients/upsclient.c: upscli_free_host_port_cert(): consider builds without SSL [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 62bc6a95dc..0ae3b86cb0 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1609,7 +1609,11 @@ void upscli_free_host_port_cert(const char* hostname, uint16_t port, const char* pthread_mutex_unlock(&mutex_host_cert); # endif } -#endif /* ! SSL */ +#else /* ! SSL */ + NUT_UNUSED_VARIABLE(hostname); + NUT_UNUSED_VARIABLE(port); + NUT_UNUSED_VARIABLE(certname); +#endif /* ! SSL */ } void upscli_free_host_cert_list(void) From 0b9ed983f8c0224e77331ed0b3bc600ca1c73753 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 02:30:16 +0200 Subject: [PATCH 086/108] clients/upsclient.c: upscli_free_host_port_cert_data(): consider const-ness of freed strings [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/upsclient.c b/clients/upsclient.c index 0ae3b86cb0..1e14a90db5 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1560,8 +1560,8 @@ static void upscli_free_host_port_cert_data(HOST_CERT_t* cert) if (!cert) return; - free(cert->host); - free(cert->certname); + 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; From 03940a283ccc7323fd250d5eb9a0b22f26568e78 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 02:35:30 +0200 Subject: [PATCH 087/108] clients/upsclient.c: upscli_free_host_port_cert_data(): only defined for SSL-capable builds [#3329] Signed-off-by: Jim Klimov --- clients/upsclient.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clients/upsclient.c b/clients/upsclient.c index 1e14a90db5..587a14cd98 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1555,6 +1555,7 @@ void upscli_free_host_cert(const char* hostname, const char* certname) port, certname); } +#if defined(WITH_OPENSSL) || defined(WITH_NSS) static void upscli_free_host_port_cert_data(HOST_CERT_t* cert) { if (!cert) @@ -1568,6 +1569,7 @@ static void upscli_free_host_port_cert_data(HOST_CERT_t* cert) cert->certname = NULL; cert->next = NULL; } +#endif /* SSL */ void upscli_free_host_port_cert(const char* hostname, uint16_t port, const char* certname) { From 188c5e146f46b8af7c1498f00ec32858fef94439 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 15:16:03 +0200 Subject: [PATCH 088/108] tests/NIT/nit.sh: use TESTCERT_PATH_SEP more diligently (e.g. on WIN32 runs) [#3331, #1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 154 ++++++++++++++++++++++++----------------------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index ea51254773..7e9dde861f 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -981,25 +981,25 @@ 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 + ls -l "${2}${TESTCERT_PATH_SEP}${3}"*.txt || true + ls -l "${2}${TESTCERT_PATH_SEP}${3}"*.db || exit ) || die "Could not list NSS ${1} DB files" # NSS certutil error handling is complicated: anything unexpected means @@ -1018,19 +1018,22 @@ 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 + if ls -l "${2}${TESTCERT_PATH_SEP}"*.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}${TESTCERT_PATH_SEP}"*.crt || true ; ls -l "${2}${TESTCERT_PATH_SEP}"*.pem "${2}${TESTCERT_PATH_SEP}"*.key "${2}${TESTCERT_PATH_SEP}".pwfile ;; Server|Client) - ls -l "${2}"/*.pem || true ; ls -l "${2}"/*.crt "${2}"/*.key "${2}"/.pwfile ;; + ls -l "${2}${TESTCERT_PATH_SEP}"*.pem || true ; ls -l "${2}${TESTCERT_PATH_SEP}"*.crt "${2}${TESTCERT_PATH_SEP}"*.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" @@ -1040,16 +1043,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 @@ -1060,12 +1063,12 @@ 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 \ + pk12util -i "${2}${TESTCERT_PATH_SEP}"*.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 @@ -1094,16 +1097,16 @@ check_NIT_certs() { case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in *OpenSSL*) - ls -l "${TESTCERT_PATH_ROOTCA}"/rootca.pem "${TESTCERT_PATH_ROOTCA}/"*.? \ + ls -l "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"*.? \ || 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 @@ -1142,19 +1145,19 @@ 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}/" + cp -pr "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}"* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" log_info "Mock certificates deployed from ${TESTCERT_MOCK_PATH}" check_NIT_certs set-none-on-fail || { @@ -1181,7 +1184,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 @@ -1199,18 +1202,19 @@ 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}/" + cp -pr "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}"* "${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 @@ -1272,7 +1276,7 @@ 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}/" + cp -pr "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}"* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" if check_NIT_certs ; then rm -f "${LOCKFILE}" @@ -1476,7 +1480,7 @@ EOF # 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}"* \ + ls -l "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}${CERTHASH}"* \ || die "Could not list OpenSSL CA PEM file and hash links" } @@ -1485,7 +1489,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 ] \ @@ -1513,7 +1517,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: @@ -1521,7 +1525,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 \ @@ -1534,7 +1538,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" \ @@ -1632,9 +1636,9 @@ EOF mkpk12key -legacy && mkjks } fi - ls -l "${TESTCERT_PATH_SERVER}"/*.jks "${TESTCERT_PATH_SERVER}"/*.p12 || true + ls -l "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.jks "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.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}" @@ -1666,19 +1670,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 ] \ @@ -1695,13 +1699,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" @@ -1716,7 +1720,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 \ @@ -1729,7 +1733,7 @@ EOF -alias "${TESTCERT_SERVER_NAME}" \ -noprompt \ && log_info "Generated Java JKS for Server (from OpenSSL)" - ls -l "${TESTCERT_PATH_SERVER}"/*.jks "${TESTCERT_PATH_SERVER}"/*.p12 || true + ls -l "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.jks "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.p12 || true fi fi ;; @@ -1750,7 +1754,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): @@ -1762,7 +1766,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" @@ -1771,7 +1775,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: @@ -1780,7 +1784,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" \ @@ -1860,25 +1864,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 ] \ @@ -1887,19 +1891,19 @@ 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}" \ -noprompt \ && log_info "Generated Java JKS truststore for Client (OpenSSL)" - ls -l "${TESTCERT_PATH_CLIENT}"/*.jks || true + ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"*.jks || true fi if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] \ @@ -1916,13 +1920,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" @@ -1930,7 +1934,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" @@ -1962,7 +1966,7 @@ EOF fi fi fi - ls -l "${TESTCERT_PATH_CLIENT}"/*.jks "${TESTCERT_PATH_CLIENT}"/*.p12 || true + ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"*.jks "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"*.p12 || true fi ;; esac @@ -1975,9 +1979,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 -pr "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}"* "${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 @@ -2001,7 +2005,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 } @@ -3552,8 +3556,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 @@ -3614,8 +3618,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 From a6568e599d75304f8649b5a37584fec22cadd746 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 17:42:23 +0200 Subject: [PATCH 089/108] tests/NIT/nit.sh: die() with a timestamp too Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 7e9dde861f..bb6236771e 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 } From 2f4e3ea36a8e892e9fd71a25daa54d6e5bcb618b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 17:43:16 +0200 Subject: [PATCH 090/108] tests/NIT/nit.sh: TESTCERT_PATH_SEP: there was too much of a good thing for WIN32 runs [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index bb6236771e..54bdc070da 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -905,6 +905,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='\\' @@ -998,8 +1000,9 @@ check_NIT_certs_NSS() { exit 0 fi - ls -l "${2}${TESTCERT_PATH_SEP}${3}"*.txt || true - ls -l "${2}${TESTCERT_PATH_SEP}${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 @@ -1024,16 +1027,17 @@ check_NIT_certs_NSS() { ; then log_warn "NSS tools on this worker need old DB format files, but we only have new ones" - if ls -l "${2}${TESTCERT_PATH_SEP}"*.p12 "${2}${TESTCERT_PATH_SEP}".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}${TESTCERT_PATH_SEP}"*.crt || true ; ls -l "${2}${TESTCERT_PATH_SEP}"*.pem "${2}${TESTCERT_PATH_SEP}"*.key "${2}${TESTCERT_PATH_SEP}".pwfile ;; + CA) ls -l "${2}"/*.crt || true ; ls -l "${2}"/*.pem "${2}"/*.key "${2}${TESTCERT_PATH_SEP}".pwfile ;; Server|Client) - ls -l "${2}${TESTCERT_PATH_SEP}"*.pem || true ; ls -l "${2}${TESTCERT_PATH_SEP}"*.crt "${2}${TESTCERT_PATH_SEP}"*.key "${2}${TESTCERT_PATH_SEP}".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" @@ -1063,7 +1067,8 @@ 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}${TESTCERT_PATH_SEP}"*.p12 -d "${2}" -k "${2}${TESTCERT_PATH_SEP}".pwfile -w "${2}${TESTCERT_PATH_SEP}".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 @@ -1097,7 +1102,8 @@ check_NIT_certs() { case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in *OpenSSL*) - ls -l "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"rootca.pem "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}"*.? \ + # 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}${TESTCERT_PATH_SEP}"upsd.pem \ @@ -1157,7 +1163,8 @@ if [ -n "${TESTCERT_MOCK_PATH-}" ] && [ -d "${TESTCERT_MOCK_PATH}" ]; then && [ -d "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}upsmon" ] \ ; then mkdir -p "${TESTCERT_PATH_BASE}" - cp -pr "${TESTCERT_MOCK_PATH}${TESTCERT_PATH_SEP}"* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" + # See comments above about no TESTCERT_PATH_SEP for shell globs. + cp -prf "${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 || { @@ -1202,7 +1209,8 @@ 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_SEP}"* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" + # See comments above about no TESTCERT_PATH_SEP for shell globs. + cp -pfr "${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_PATH_SEP}TESTCERT_VARS.env" ]; then @@ -1276,7 +1284,8 @@ 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_SEP}"* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" + # See comments above about no TESTCERT_PATH_SEP for shell globs. + cp -prf "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" if check_NIT_certs ; then rm -f "${LOCKFILE}" @@ -1478,9 +1487,12 @@ EOF || 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 + log_info "SSL: Preparing OpenSSL CA PEM file hash-named (${CERTHASH}) copies or links" 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}${TESTCERT_PATH_SEP}"rootca.pem "${TESTCERT_PATH_ROOTCA}${TESTCERT_PATH_SEP}${CERTHASH}"* \ + + # 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" } @@ -1636,7 +1648,8 @@ EOF mkpk12key -legacy && mkjks } fi - ls -l "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.jks "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.p12 || true + # 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}${TESTCERT_PATH_SEP}"rootca.pem server.key > upsd.pem 2>/dev/null || true fi @@ -1733,7 +1746,8 @@ EOF -alias "${TESTCERT_SERVER_NAME}" \ -noprompt \ && log_info "Generated Java JKS for Server (from OpenSSL)" - ls -l "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.jks "${TESTCERT_PATH_SERVER}${TESTCERT_PATH_SEP}"*.p12 || true + # See comments above about no TESTCERT_PATH_SEP for shell globs. + ls -l "${TESTCERT_PATH_SERVER}"/*.jks "${TESTCERT_PATH_SERVER}"/*.p12 || true fi fi ;; @@ -1903,7 +1917,7 @@ EOF -storepass "${TESTCERT_ROOTCA_PASS}" \ -noprompt \ && log_info "Generated Java JKS truststore for Client (OpenSSL)" - ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"*.jks || true + ls -l "${TESTCERT_PATH_CLIENT}"/*.jks || true fi if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] \ @@ -1966,7 +1980,7 @@ EOF fi fi fi - ls -l "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"*.jks "${TESTCERT_PATH_CLIENT}${TESTCERT_PATH_SEP}"*.p12 || true + ls -l "${TESTCERT_PATH_CLIENT}"/*.jks "${TESTCERT_PATH_CLIENT}"/*.p12 || true fi ;; esac @@ -1979,7 +1993,7 @@ 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}${TESTCERT_PATH_SEP}"* "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}" + cp -prf "${TESTCERT_PATH_BASE}"/* "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}" set | ${EGREP} '^TESTCERT[^ ]*=' | grep -v PATH \ > "${CI_CACHE_NIT_HASHDIR}${TESTCERT_PATH_SEP}TESTCERT_VARS.env" fi From fb4f7a59fce39d8e9f0eaec9bfb4a12dc5fe3723 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 17:59:46 +0200 Subject: [PATCH 091/108] tests/NIT/nit.sh: when transplanting prepared certs to/from cache, fall back to dereferencing symlinks if `cp -prf` failed [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 54bdc070da..090be9aaff 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -1164,7 +1164,8 @@ if [ -n "${TESTCERT_MOCK_PATH-}" ] && [ -d "${TESTCERT_MOCK_PATH}" ]; then ; then mkdir -p "${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 -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 || { @@ -1210,7 +1211,8 @@ if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] ; then log_info "Found cached NIT certificates in ${CI_CACHE_NIT_HASHDIR}" mkdir -p "${TESTCERT_PATH_BASE}" # See comments above about no TESTCERT_PATH_SEP for shell globs. - cp -pfr "${CI_CACHE_NIT_HASHDIR}"/* "${TESTCERT_PATH_BASE}${TESTCERT_PATH_SEP}" + 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_PATH_SEP}TESTCERT_VARS.env" ]; then @@ -1285,7 +1287,8 @@ case "${WITH_SSL_CLIENT}${WITH_SSL_SERVER}" in log_info "Found cached NIT certificates in ${CI_CACHE_NIT_HASHDIR} after waiting" mkdir -p "${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 -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}" From 04b9ffa783b192f18dd5d4868bb5d4e412f235f8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 23 Jun 2026 18:00:24 +0200 Subject: [PATCH 092/108] tests/NIT/nit.sh: on MSYS2, copy (not symlink) root CA cert hash-named files right away [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 090be9aaff..2f916f35b7 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -1489,10 +1489,19 @@ 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 + # 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" - 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}" + 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}"* \ From 8e287562e0266d749c2575a776096a7b8202b83b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 15:46:17 +0200 Subject: [PATCH 093/108] tests/NIT/nit.sh: move check for usability of `certutil` or `openssl` into prepare_NIT_certs() where we would inevitably use them [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 55 +++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 2f916f35b7..bcaaa9c782 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -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" @@ -1223,6 +1197,7 @@ if [ x"${DO_USE_NIT_TESTCERT_CACHE-}" = xyes ] ; then fi check_NIT_certs && return + 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 @@ -1234,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. From e0663df36210def53fef1096f9b31f568fc8bbd8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 15:53:13 +0200 Subject: [PATCH 094/108] tests/NIT/nit.sh: generatecfg_nutauth(): only handle `case WITH_SSL_CLIENT_CERTIDENT ...` if `WITH_SSL_CLIENT != none` [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index bcaaa9c782..2315a85e72 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2493,6 +2493,7 @@ 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 From 95c930752ad28427751b09ba91911a8b55f1c5ca Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 17:42:56 +0200 Subject: [PATCH 095/108] tests/test_authconf.c: auto-account expected_sections (to check parsed num_sections) [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 88c78c9e65..6099e49b19 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -25,7 +25,7 @@ int main(int argc, char **argv) const char *include_conf = "test_include.conf"; FILE *f; upscli_authconf_t *ac, *ac5, *ac7, *ac8, *ac9, *ac12; - size_t num_sections; + size_t num_sections, expected_sections = 0; char buf[512], *s; int l, testnum = 0; @@ -46,13 +46,19 @@ int main(int argc, char **argv) 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"); @@ -63,6 +69,8 @@ int main(int argc, char **argv) perror("fopen test_include.conf"); return 1; } + + expected_sections++; fprintf(f, "[@otherhost]\n"); fprintf(f, " USER = otheruser\n"); fprintf(f, " CERTHOST = \"Other Server\"\n"); @@ -93,14 +101,14 @@ int main(int argc, char **argv) /* Not "for_debug", but how would this info look in a config file */ num_sections = upscli_dump_authconf_list(NULL, 0, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); - printf("%sok %d - parsed 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); + 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 4 sections\n", num_sections == 4 ? "" : "not ", ++testnum); + printf("%sok %d - parsed %" PRIuSIZE " sections (including global)\n", num_sections == expected_sections ? "" : "not ", ++testnum, expected_sections); /* Test matching */ printf("=== Testing matches...\n"); @@ -108,6 +116,7 @@ int main(int argc, char **argv) /* 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) { @@ -181,7 +190,7 @@ int main(int argc, char **argv) return 1; } - /* 8. Host default match, saved to list */ + /* 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) { @@ -194,6 +203,7 @@ int main(int argc, char **argv) /* 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", @@ -320,7 +330,8 @@ int main(int argc, char **argv) num_sections = upscli_dump_authconf_list(NULL, 0, 1); printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); /* Added '@somehost:port' and 'somebody@...' */ - printf("%sok %d - parsed 6 sections\n", num_sections == 6 ? "" : "not ", ++testnum); + 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); From bec6a33a64b49295eba0509534d572baf62d5fd9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 17:43:26 +0200 Subject: [PATCH 096/108] tests/test_authconf.c: test that (comment) text after a section name is ignored [#3329] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index 6099e49b19..f73285b643 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -70,8 +70,9 @@ int main(int argc, char **argv) return 1; } + /* NOTE: Non-commented tokens are probably also ignored */ expected_sections++; - fprintf(f, "[@otherhost]\n"); + fprintf(f, "[@otherhost] # Other (commented) tokens ignored\n"); fprintf(f, " USER = otheruser\n"); fprintf(f, " CERTHOST = \"Other Server\"\n"); fclose(f); From 39a5a5c5487bc4821c4a1098bd6326715bc526d5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 17:44:01 +0200 Subject: [PATCH 097/108] tests/test_authconf.c: add tests to parse IPv6 addresses (brackets and colons!) [#3329, #3503] Signed-off-by: Jim Klimov --- tests/test_authconf.c | 226 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 1 deletion(-) diff --git a/tests/test_authconf.c b/tests/test_authconf.c index f73285b643..2ff2df8bf7 100644 --- a/tests/test_authconf.c +++ b/tests/test_authconf.c @@ -75,6 +75,33 @@ int main(int argc, char **argv) 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"))) { @@ -325,7 +352,204 @@ int main(int argc, char **argv) printf("ok %d - No bogus match kind of OK: got no ac\n", ++testnum); } - /* 16. Expected printout 3 */ + /* 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); From 03235cebd4dc7f77c77f0b250eedd6a6671a8420 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 18:17:40 +0200 Subject: [PATCH 098/108] clients/authconf.c: parse bracketed IPv6 addresses correctly [#3329, #3503] Signed-off-by: Jim Klimov --- clients/authconf.c | 87 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/clients/authconf.c b/clients/authconf.c index aff088a44d..2c7b9a68b8 100644 --- a/clients/authconf.c +++ b/clients/authconf.c @@ -588,26 +588,101 @@ int upscli_split_authconf_section(const char *sect_name, /* 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; + 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; } @@ -722,8 +797,10 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) current_section_ignored = 0; - sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ - end_bracket = strchr(sect_name, ']'); + /* 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); @@ -740,7 +817,9 @@ static void handle_authconf_args(size_t numargs, char **arg, int global_scope) return; } - *(char *)(end_bracket) = '\0'; /* forget trailing ']' and any characters after it (comments etc.) */ + /* 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, From 2ae696e46f3f561c4b8520a6f54189c59ba05315 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 7 Jun 2026 18:42:23 +0200 Subject: [PATCH 099/108] clients/nutclient*.{h,cpp}, scripts/perl/UPS/Nut.pm, scripts/python/module/PyNUT.py.in: add ability to parse nutauth.conf files [#3329] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 278 +++++++++++++++++++++++++++++- clients/nutclient.h | 57 ++++++ clients/nutclientmem.h | 3 + scripts/perl/UPS/Nut.pm | 157 ++++++++++++++++- scripts/python/module/PyNUT.py.in | 170 ++++++++++++++++++ 5 files changed, 663 insertions(+), 2 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 27b416a615..395d38d9a0 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -25,10 +25,18 @@ #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 @@ -1992,6 +2000,248 @@ 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 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; + + if (arg[0][0] == '[' && arg[0][strlen(arg[0])-1] == ']') { + std::string sectname = arg[0]; + sectname = sectname.substr(1, sectname.length() - 2); + + if (sectname == "*") { + 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 { + authconf_list.push_back(AuthConf(sectname)); + current_section = &authconf_list.back(); + } + 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 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; + + size_t open_bracket = buf.find('['); + size_t close_bracket = buf.find(']'); + size_t colon; + + if (open_bracket != std::string::npos && close_bracket != std::string::npos && open_bracket < close_bracket) { + hostname = buf.substr(open_bracket + 1, close_bracket - open_bracket - 1); + colon = buf.find(':', close_bracket); + } else { + colon = buf.find(':'); + if (colon != std::string::npos) { + hostname = buf.substr(0, colon); + } else { + hostname = buf; + } + } + + if (colon != std::string::npos && colon + 1 < buf.length()) { + port = static_cast(strtol(buf.substr(colon + 1).c_str(), nullptr, 10)); + } else { + port = NUT_PORT; + } + + 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 = user + "@" + host + ":" + port; +} + +/*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()) { + return global_defaults ? *global_defaults : 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 + if (!user.empty()) { + std::string userless_name = "@" + norm_host + ":" + norm_port; + 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) +{ + AuthConf res = findAuthConf(user, host, port); + // If it's a new item or global defaults, we might want to merge, but findAuthConf already returns the best match. + // In the C implementation, get_authconf_item can create a new entry and merge defaults. + // For simplicity, we return the find result. + return res; +} + +/*static*/ void AuthConf::freeAuthConfList() +{ + authconf_list.clear(); + global_defaults = nullptr; +} + /* * * TCP Client implementation @@ -3242,6 +3492,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); @@ -3558,6 +3829,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 e73ee53a57..f02ebc5e4c 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -87,6 +87,50 @@ 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). + */ + static AuthConf getAuthConf(const std::string& user, const std::string& host, const std::string& port); + + /** Clear the global list of authentication configurations */ + static void freeAuthConfList(); + + std::string section; + std::string user; + std::string pass; + std::string certpath; + std::string certfile; + std::string certident; + std::string certpasswd; + std::string ssl_backend; + 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). @@ -820,6 +864,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 ? @@ -1123,6 +1173,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). @@ -1169,6 +1225,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/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 36ccc26d6d..cb029e4410 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -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,155 @@ This module is distributed under the same license as Perl itself. =cut +package UPS::Nut::AuthConf; + +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; + + if (!defined $filename) { + my $confpath = $ENV{NUT_CONFPATH} || "/etc/nut"; + $filename = "$confpath/nutauth.conf"; + } + + if (!-e $filename) { + die "Could not open $filename" if $fatal_errors; + return (); + } + + if (!open(my $fh, '<', $filename)) { + die "Error opening $filename: $!" if $fatal_errors; + return (); + } + + my @auth_configs; + my $current_ac; + + while (my $line = <$fh>) { + chomp $line; + $line =~ s/^\s+|\s+$//g; + next if !$line || $line =~ /^#/; + + if ($line =~ /^\[(.*)\]$/) { + $current_ac = UPS::Nut::AuthConf->new($line); + push @auth_configs, $current_ac; + next; + } + + next if !$current_ac; + + if ($line =~ /^([^=]+)=(.*)$/) { + my ($key, $value) = ($1, $2); + $key =~ s/^\s+|\s+$//g; + $key = lc($key); + $value =~ s/^\s+|\s+$//g; + $value =~ s/^["']|["']$//g; + + 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 @auth_configs; +} + +sub findAuthConf { + my $class = shift; + my ($user, $host, $port, $auth_configs_ref) = @_; + my @auth_configs = $auth_configs_ref ? @$auth_configs_ref : $class->readAuthConfFile(); + + 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 || 'localhost'; + $target .= ":$port" if defined $port; + + return $ac if $section eq $target; + + # fallback matches + if (!defined $user && defined $host && defined $port && $section eq "$host:$port") { + return $ac; + } + if (!defined $user && !defined $port && defined $host && $section eq $host) { + 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 ($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..3402180ef7 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,124 @@ class TrackingID: def isValid(self): return self.__id is not None and self.__id != "" +class AuthConf: + """ Authentication configuration for a NUT client. """ + 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): + """ Read the authentication configuration file (usually nutauth.conf) """ + if filename is None: + # Matches C implementation logic for default path + filename = os.path.join(os.environ.get("NUT_CONFPATH", "/etc/nut"), "nutauth.conf") + + if not os.path.exists(filename): + if fatal_errors: + raise PyNUTError(f"Could not open {filename}") + return [] + + auth_configs = [] + current_ac = 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(']'): + current_ac = AuthConf(line) + auth_configs.append(current_ac) + continue + + if current_ac is None: + # Global settings if any, but NUT usually expects sections + 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 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 auth_configs + + @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.readAuthConfFile() + + # Try to match [user@host:port], then [host:port], then [host], then [*] + # This is a simplified version of the C logic + matches = [] + 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 if host else 'localhost'}" + if port: target += f":{port}" + + if section == target: + return ac + + # 2. Match host:port + if not user and section == f"{host}:{port}": + return ac + + # 3. Match host + if not user and not port and section == host: + return ac + + if section == "*": + matches.append(ac) + + if matches: + return matches[0] + + return None + class PyNUTError( Exception ) : """ Base class for custom exceptions """ @@ -118,6 +238,56 @@ 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 ':' 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 From 2af7b8a94c794808fb3198474a056c1d5578eac6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 7 Jun 2026 19:20:33 +0200 Subject: [PATCH 100/108] include/nutconf.hpp, common/nutconf.cpp: add ability to parse nutauth.conf files [#3329] Signed-off-by: Jim Klimov --- common/nutconf.cpp | 71 +++++++++++++++++++++++++++++++++++++++++++++ include/nutconf.hpp | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) 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/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 */ From ea1092609854def5757bce2fb365a86d7fc64342 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 8 Jun 2026 03:09:19 +0200 Subject: [PATCH 101/108] clients/authconf.h, clients/nutclient.{h,cpp}, scripts/perl/UPS/Nut.pm, scripts/python/module/PyNUT.py.in: revise ability to parse nutauth.conf files [#3329] Signed-off-by: Jim Klimov --- clients/authconf.h | 10 +- clients/nutclient.cpp | 189 ++++++++++++++++++++++++++---- clients/nutclient.h | 31 ++++- scripts/perl/UPS/Nut.pm | 178 +++++++++++++++++++++++++--- scripts/python/module/PyNUT.py.in | 181 ++++++++++++++++++++++++---- 5 files changed, 522 insertions(+), 67 deletions(-) diff --git a/clients/authconf.h b/clients/authconf.h index dfbcf55123..ea577fd963 100644 --- a/clients/authconf.h +++ b/clients/authconf.h @@ -24,12 +24,12 @@ 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 *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; + 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 */ diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 395d38d9a0..e97dc9b0b3 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -26,16 +26,17 @@ #include "config.h" #include "nutclient.h" #include "parseconf.h" + #include #include #include #include #ifndef WIN32 -#include +# include #else -#include -#define R_OK 4 +# include +# define R_OK 4 #endif /* TODO: Make it a run-time option like upsdebugx(), @@ -245,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; @@ -2052,26 +2053,54 @@ static void set_authconf_val(AuthConf& conf, const std::string& var, const std:: } } +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 == "*") { + + if (sectname == "_global_defaults" || sectname.empty()) { if (!global_defaults) { - global_defaults = new AuthConf("*"); + 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 { - authconf_list.push_back(AuthConf(sectname)); - current_section = &authconf_list.back(); + /* 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; } @@ -2083,7 +2112,7 @@ static void handle_authconf_args(size_t numargs, char **arg, AuthConf*& current_ if (!current_section) { if (global_scope) { if (!global_defaults) { - global_defaults = new AuthConf("*"); + global_defaults = new AuthConf(""); authconf_list.push_back(*global_defaults); global_defaults = &authconf_list.back(); } @@ -2143,7 +2172,7 @@ static int parse_authconf_file(const std::string& filename, int fatal_errors, bo 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; @@ -2151,7 +2180,9 @@ static int parse_authconf_file(const std::string& filename, int fatal_errors, bo } if (fn.empty()) { - if (fatal_errors) throw nut::IOException("Can't open a default nutauth.conf file"); + if (fatal_errors) { + throw nut::IOException("Can't open a user/site-provided default nutauth.conf file"); + } return -1; } @@ -2198,12 +2229,62 @@ static void normalize_parts(std::string& normalized_name, std::string& user, std normalized_name = user + "@" + host + ":" + 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 { + /* No '@' or no username chars before it 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()) { - return global_defaults ? *global_defaults : AuthConf(); + 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; @@ -2216,23 +2297,85 @@ static void normalize_parts(std::string& normalized_name, std::string& user, std if (ac.section == normalized_name) return ac; } - // Retry without user if we have one - if (!user.empty()) { - std::string userless_name = "@" + norm_host + ":" + norm_port; - for (const auto& ac : authconf_list) { - if (ac.section == userless_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) +/*static*/ AuthConf AuthConf::getAuthConf(const std::string& user, const std::string& host, const std::string& port, bool add_to_list) { - AuthConf res = findAuthConf(user, host, port); - // If it's a new item or global defaults, we might want to merge, but findAuthConf already returns the best match. - // In the C implementation, get_authconf_item can create a new entry and merge defaults. - // For simplicity, we return the find result. + 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; } diff --git a/clients/nutclient.h b/clients/nutclient.h index f02ebc5e4c..3dd11b0182 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -108,20 +108,49 @@ class AuthConf /** 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); + 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 */ diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index cb029e4410..34a85cee5b 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -1506,6 +1506,18 @@ This module is distributed under the same license as Perl itself. 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 || ""; @@ -1530,24 +1542,48 @@ 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 $confpath = $ENV{NUT_CONFPATH} || "/etc/nut"; - $filename = "$confpath/nutauth.conf"; + 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} || "/etc/nut"; + if (-r "$confpath/nutauth.conf") { + $filename = "$confpath/nutauth.conf"; + } + } } - if (!-e $filename) { - die "Could not open $filename" if $fatal_errors; + 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 (); } - if (!open(my $fh, '<', $filename)) { + my $fh; + if (!open($fh, '<', $filename)) { die "Error opening $filename: $!" if $fatal_errors; return (); } - my @auth_configs; 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; @@ -1555,12 +1591,41 @@ sub readAuthConfFile { next if !$line || $line =~ /^#/; if ($line =~ /^\[(.*)\]$/) { - $current_ac = UPS::Nut::AuthConf->new($line); - push @auth_configs, $current_ac; + 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; } - next if !$current_ac; + # 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); @@ -1569,6 +1634,18 @@ sub readAuthConfFile { $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; } @@ -1582,13 +1659,84 @@ sub readAuthConfFile { } } close($fh); - return @auth_configs; + 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 $target_user_host_port = ""; + $target_user_host_port .= "$user\@" if defined $user; + $target_user_host_port .= $norm_host; + $target_user_host_port .= ":$norm_port" if defined $norm_port; + + my $target_host_port = "\@$norm_host"; + $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 : $class->readAuthConfFile(); + my @auth_configs = $auth_configs_ref ? @$auth_configs_ref : ( @authconf_list ? @authconf_list : $class->readAuthConfFile() ); my $star_match; foreach my $ac (@auth_configs) { @@ -1602,7 +1750,7 @@ sub findAuthConf { $target .= ":$port" if defined $port; return $ac if $section eq $target; - + # fallback matches if (!defined $user && defined $host && defined $port && $section eq "$host:$port") { return $ac; @@ -1621,7 +1769,7 @@ sub findAuthConf { sub to_nut_args { my $self = shift; my %args; - + # Extract HOST and PORT from section my $host = 'localhost'; my $port = '3493'; @@ -1636,7 +1784,7 @@ sub to_nut_args { } elsif ($sect ne "") { $host = $sect; } - + $args{HOST} = $host; $args{PORT} = $port; $args{USERNAME} = $self->{user} if defined $self->{user}; @@ -1649,7 +1797,7 @@ sub to_nut_args { $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; } diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 3402180ef7..204f8fb75d 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -115,6 +115,20 @@ class TrackingID: 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 @@ -129,19 +143,37 @@ class AuthConf: self.forcessl = -1 @staticmethod - def readAuthConfFile(filename=None, fatal_errors=False): + 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 - filename = os.path.join(os.environ.get("NUT_CONFPATH", "/etc/nut"), "nutauth.conf") + 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"Could not open {filename}") - return [] + 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 - auth_configs = [] - current_ac = None + current_ac = AuthConf.__global_defaults if global_scope else None try: with open(filename, 'r') as f: @@ -151,12 +183,38 @@ class AuthConf: continue if line.startswith('[') and line.endswith(']'): - current_ac = AuthConf(line) - auth_configs.append(current_ac) + 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 - if current_ac is None: - # Global settings if any, but NUT usually expects sections + # 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: @@ -168,6 +226,15 @@ class AuthConf: (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': @@ -192,29 +259,103 @@ class AuthConf: if fatal_errors: raise PyNUTError(f"Error reading {filename}: {str(e)}") - return auth_configs + 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 + + target_user_host_port = "" + if user: target_user_host_port += f"{user}@" + target_user_host_port += f"{norm_host}" + if norm_port: target_user_host_port += f":{norm_port}" + + target_host_port = f"@{norm_host}" + 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.readAuthConfFile() + auth_configs = AuthConf.__authconf_list if AuthConf.__authconf_list else AuthConf.readAuthConfFile() # Try to match [user@host:port], then [host:port], then [host], then [*] # This is a simplified version of the C logic - matches = [] 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 if host else 'localhost'}" if port: target += f":{port}" - + if section == target: return ac - + # 2. Match host:port if not user and section == f"{host}:{port}": return ac @@ -223,13 +364,7 @@ class AuthConf: if not user and not port and section == host: return ac - if section == "*": - matches.append(ac) - - if matches: - return matches[0] - - return None + return AuthConf.__global_defaults class PyNUTError( Exception ) : """ Base class for custom exceptions """ From 6d0590476247e60b6944389218820a86229932f7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 19:49:17 +0200 Subject: [PATCH 102/108] Convert UPS/Nut.pm into a .in template to handle @CONFPATH@ correctly [#1711, #3329] Signed-off-by: Jim Klimov --- configure.ac | 1 + scripts/Makefile.am | 2 +- scripts/perl/UPS/{Nut.pm => Nut.pm.in} | 2 +- tests/NIT/Makefile.am | 2 ++ tests/NIT/nit.sh | 4 ++-- 5 files changed, 7 insertions(+), 4 deletions(-) rename scripts/perl/UPS/{Nut.pm => Nut.pm.in} (99%) diff --git a/configure.ac b/configure.ac index fd5d832d7b..3d41ca5f34 100644 --- a/configure.ac +++ b/configure.ac @@ -7398,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/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/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm.in similarity index 99% rename from scripts/perl/UPS/Nut.pm rename to scripts/perl/UPS/Nut.pm.in index 34a85cee5b..4b20a93038 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm.in @@ -1560,7 +1560,7 @@ sub readAuthConfFile { } if (!defined $filename) { - my $confpath = $ENV{NUT_CONFPATH} || "/etc/nut"; + my $confpath = $ENV{NUT_CONFPATH} || "@CONFPATH@"; if (-r "$confpath/nutauth.conf") { $filename = "$confpath/nutauth.conf"; } diff --git a/tests/NIT/Makefile.am b/tests/NIT/Makefile.am index 2836dc1bd9..da36847def 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" && $(MAKE) $(AM_MAKEFLAGS) -s python/module/PyNUT.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 2315a85e72..f56fda812d 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -3860,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 @@ -3890,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 From f37b105142ea3df1cc89ab68a1966baf5370b0ad Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 21:31:47 +0200 Subject: [PATCH 103/108] scripts/python/module/PyNUT.py.in: allow to construct PyNUTClient without connecting right away; remember requested TRACKING option to apply whenever we do connect() [#3329, #1711] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 204f8fb75d..054c59cde1 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -443,7 +443,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 @@ -453,8 +454,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) @@ -586,9 +588,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 ) : @@ -704,6 +710,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') From 4fd59768a017be7113ac3abaf06c5145f2d45960 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 20:14:25 +0200 Subject: [PATCH 104/108] scripts/python/module/test_nutclient.py: add testsuite_splitaddr() [#3329, #1711] Signed-off-by: Jim Klimov --- scripts/python/module/test_nutclient.py.in | 36 ++++++++++++++++++++++ tests/NIT/Makefile.am | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) 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/tests/NIT/Makefile.am b/tests/NIT/Makefile.am index da36847def..d17ae02a98 100644 --- a/tests/NIT/Makefile.am +++ b/tests/NIT/Makefile.am @@ -64,7 +64,7 @@ check-NIT-devel: $(abs_srcdir)/nit.sh @dotMAKE@ +@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" && $(MAKE) $(AM_MAKEFLAGS) -s python/module/PyNUT.py + +@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 From 881d286d6ebe6fa2153149897ba2205c8b34dd86 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 20:55:40 +0200 Subject: [PATCH 105/108] clients/nutclient.cpp: update splitaddr() against libupsclient variant Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 54 ++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index e97dc9b0b3..1ca7a65fba 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2193,26 +2193,54 @@ static int splitaddr(const std::string& buf, std::string& hostname, uint16_t& po { if (buf.empty()) return -1; - size_t open_bracket = buf.find('['); - size_t close_bracket = buf.find(']'); - size_t colon; - - if (open_bracket != std::string::npos && close_bracket != std::string::npos && open_bracket < close_bracket) { - hostname = buf.substr(open_bracket + 1, close_bracket - open_bracket - 1); - colon = buf.find(':', close_bracket); + 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 = buf.find(':'); + colon = tmp.find(':'); if (colon != std::string::npos) { - hostname = buf.substr(0, colon); + hostname = tmp.substr(0, colon); } else { - hostname = buf; + hostname = tmp; + port = NUT_PORT; + return 0; } } - if (colon != std::string::npos && colon + 1 < buf.length()) { - port = static_cast(strtol(buf.substr(colon + 1).c_str(), nullptr, 10)); + 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 { - port = NUT_PORT; + fprintf(stderr, "splitaddr: no port number specified after ':' separator: %s\n", buf.c_str()); + return -1; } return 0; From be9de68c3a43371c94e2b1c654e8a3d68d099fad Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 21:01:46 +0200 Subject: [PATCH 106/108] clients/nutclient.cpp, scripts/perl/UPS/Nut.pm.in, scripts/python/module/PyNUT.py.in: revise normalization of host strings that look like numeric IPv6 [#3503] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 17 +++++++++++++++-- scripts/perl/UPS/Nut.pm.in | 25 +++++++++++++++++++------ scripts/python/module/PyNUT.py.in | 25 +++++++++++++++++++------ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 1ca7a65fba..3526acad7d 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2209,7 +2209,7 @@ static int splitaddr(const std::string& buf, std::string& hostname, uint16_t& po 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; @@ -2254,7 +2254,20 @@ static void normalize_parts(std::string& normalized_name, std::string& user, std snprintf(portbuf, sizeof(portbuf), "%u", static_cast(NUT_PORT)); port = portbuf; } - normalized_name = user + "@" + host + ":" + port; + + 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) diff --git a/scripts/perl/UPS/Nut.pm.in b/scripts/perl/UPS/Nut.pm.in index 4b20a93038..b906217452 100644 --- a/scripts/perl/UPS/Nut.pm.in +++ b/scripts/perl/UPS/Nut.pm.in @@ -1695,12 +1695,17 @@ sub getAuthConf { 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 .= $norm_host; + $target_user_host_port .= $host_part; $target_user_host_port .= ":$norm_port" if defined $norm_port; - my $target_host_port = "\@$norm_host"; + 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]"); @@ -1738,6 +1743,14 @@ sub findAuthConf { 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}; @@ -1746,16 +1759,16 @@ sub findAuthConf { my $target = ""; $target .= "$user\@" if defined $user; - $target .= $host || 'localhost'; - $target .= ":$port" if defined $port; + $target .= $host_part; + $target .= ":$norm_port" if defined $norm_port; return $ac if $section eq $target; # fallback matches - if (!defined $user && defined $host && defined $port && $section eq "$host:$port") { + if (!defined $user && defined $norm_port && $section eq "$host_part:$norm_port") { return $ac; } - if (!defined $user && !defined $port && defined $host && $section eq $host) { + if (!defined $user && !defined $norm_port && $section eq $host_part) { return $ac; } diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 054c59cde1..45d72a7711 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -306,12 +306,17 @@ class AuthConf: 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"{norm_host}" + target_user_host_port += f"{host_part}" if norm_port: target_user_host_port += f":{norm_port}" - target_host_port = f"@{norm_host}" + target_host_port = f"@{host_part}" if norm_port: target_host_port += f":{norm_port}" res = AuthConf(f"[{target_user_host_port}]") @@ -342,6 +347,14 @@ class AuthConf: 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: @@ -350,18 +363,18 @@ class AuthConf: # 1. Exact match user@host:port target = "" if user: target += f"{user}@" - target += f"{host if host else 'localhost'}" - if port: target += f":{port}" + 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}:{port}": + if not user and section == f"{host_part}:{norm_port}": return ac # 3. Match host - if not user and not port and section == host: + if not user and not norm_port and section == host_part: return ac return AuthConf.__global_defaults From a46e0fb6e9fbfc7d35343d8eb7aae08f761939c7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 21:02:54 +0200 Subject: [PATCH 107/108] clients/nutclient.cpp: AuthConf::merge(): gracefully handle section name that starts with "@" [#3329] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 3526acad7d..56938d5957 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2280,8 +2280,10 @@ void AuthConf::merge(const AuthConf& source) 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 '@' or no username chars before it in target section title */ + /* No '@' in target section title */ if (user.empty() && !source.user.empty()) { user = source.user; } From cb172e1e030f470a3fe1a4de3f9aff7ad110aa6b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 24 Jun 2026 21:03:54 +0200 Subject: [PATCH 108/108] scripts/perl/UPS/Nut.pm.in, scripts/python/module/PyNUT.py.in: revise splitting of host:port strings where host looks like numeric IPv6 [#3503] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm.in | 13 ++++++++++++- scripts/python/module/PyNUT.py.in | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm.in b/scripts/perl/UPS/Nut.pm.in index b906217452..5fa3026475 100644 --- a/scripts/perl/UPS/Nut.pm.in +++ b/scripts/perl/UPS/Nut.pm.in @@ -1792,7 +1792,18 @@ sub to_nut_args { if ($sect =~ /@/) { $sect =~ s/^[^@]*@//; } - if ($sect =~ /:/) { + + 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; diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 45d72a7711..7f38b0d5b5 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -405,7 +405,20 @@ class PyNUTClient : addr_part = section if addr_part: - if ':' in 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: