diff --git a/NEWS.adoc b/NEWS.adoc index 1b59dc78b9..aa5fde67ed 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -99,6 +99,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 f26e5a645a..fa2730bccb 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -110,7 +110,7 @@ endif HAVE_CXX11 # Optionally deliverable as part of NUT public API: if WITH_DEV - include_HEADERS = upsclient.h + include_HEADERS = upsclient.h authconf.h if HAVE_CXX11 include_HEADERS += nutclient.h nutclientmem.h else !HAVE_CXX11 @@ -170,7 +170,7 @@ upsstats_cgi_LDADD = $(LDADD_CLIENT) $(top_builddir)/common/libcommonstrjson.la # but it needs nut_version.h made before the rest of build, # to include it into upsclient.c (without an explicit link, # this target is sometimes missed in parallel builds): -libupsclient_la_SOURCES = upsclient.c upsclient.h +libupsclient_la_SOURCES = upsclient.c upsclient.h authconf.c authconf.h # See comments for similar trick in common/Makefile.am for common-nut_version.c if BUILDING_IN_TREE diff --git a/clients/authconf.c b/clients/authconf.c new file mode 100644 index 0000000000..d0db4b9f49 --- /dev/null +++ b/clients/authconf.c @@ -0,0 +1,1215 @@ +/* authconf.c - handling NUT client authentication configuration parsing + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" +#include "common.h" + +#include "authconf.h" +#include "parseconf.h" +#include "upsclient.h" + +#include +#include +#include +#include +#include + +#ifndef WIN32 +# include +# include +# include +# include +#else /* => WIN32 */ +/* Those 2 files for support of getaddrinfo, getnameinfo and freeaddrinfo + on Windows 2000 and older versions */ +# include +# include +/* This override network system calls to adapt to Windows specificity */ +# define W32_NETWORK_CALL_OVERRIDE +# include "wincompat.h" +# undef W32_NETWORK_CALL_OVERRIDE +#endif /* WIN32 */ + +static upscli_authconf_t *authconf_list = NULL; +/** Shortcut: link to the section in authconf_list whose lines we are currently + * editing in the configuration reader; if NULL, we are editing global defaults */ +static upscli_authconf_t *current_section = NULL; +/** Shortcut: link to the (probably first) section in authconf_list with null + * "section" name */ +static upscli_authconf_t *global_defaults = NULL; +/** Does the section title of current_section include a non-trivial "user" + * name component (would we ignore a USER directive, if present)? */ +static int current_section_with_fixed_username = 0; +/** Is the section title of current_section ignored in the configuration reader + * (e.g. ignored because of a section-scope directive and does not match the + * name of current_section after normalization, or a reserved title for the + * global section while not in its context)? */ +static int current_section_ignored = 0; + +static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope); + +upscli_authconf_t *upscli_get_authconf_list(void) +{ + return authconf_list; +} + +upscli_authconf_t *upscli_create_authconf_item(const char *section) +{ + upscli_authconf_t *node = (upscli_authconf_t *)calloc(1, sizeof(upscli_authconf_t)); + + if (!node) { + upsdebugx(1, "Failed to create nutauth configuration node for section '%s'", NUT_STRARG(section)); + return NULL; + } + + if (section) { + /* FIXME: normalize section */ + node->section = xstrdup(section); + } + node->certverify = -1; + node->forcessl = -1; + + return node; +} + +upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const char *section) +{ + upscli_authconf_t *node = upscli_create_authconf_item(section); + + if (!node) { + upsdebugx(1, "Failed to create nutauth configuration node for section '%s'", NUT_STRARG(section)); + return NULL; + } + + if (source) { + const char *at = NULL; + + if (!section) + node->section = source->section ? xstrdup(source->section) : NULL; + + if ( ((at = strchr(node->section, '@')) != NULL) + && at != node->section + ) { + /* New section title strictly defines a user name */ + node->user = (char*)xcalloc(at - node->section + 1, sizeof(char)); + memcpy(node->user, node->section, at - node->section); + } else { + /* No '@' or no username chars before it */ + node->user = source->user ? xstrdup(source->user) : NULL; + } + + node->pass = source->pass ? xstrdup(source->pass) : NULL; + node->certpath = source->certpath ? xstrdup(source->certpath) : NULL; + node->certfile = source->certfile ? xstrdup(source->certfile) : NULL; + node->certident = source->certident ? xstrdup(source->certident) : NULL; + node->certpasswd = source->certpasswd ? xstrdup(source->certpasswd) : NULL; + node->ssl_backend = source->ssl_backend ? xstrdup(source->ssl_backend) : NULL; + + node->certhost = source->certhost ? xstrdup(source->certhost) : NULL; + node->certverify = source->certverify; + node->forcessl = source->forcessl; + } + + return node; +} + +/** Merge contents of two existing configuration items, they may be or not be on the list */ +upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_authconf_t *target) +{ + const char *at = NULL; + + if (!source) + return target; + + /* TOTHINK: (re-)normalize? */ + if ( (!(target->section) || !*(target->section)) + && (source->section && *(source->section)) + ) { + free(target->section); + target->section = xstrdup(source->section); + } + + if ( ((at = strchr(target->section, '@')) != NULL) + && at != target->section + ) { + /* Target section title strictly defines a user name */ + free(target->user); + target->user = (char*)xcalloc(at - target->section + 1, sizeof(char)); + memcpy(target->user, target->section, at - target->section); + } else { + /* No '@' or no username chars before it in target section title */ + if (!(target->user) && source->user) { + target->user = xstrdup(source->user); + } /* else keep what was there */ + } + + /* Replace only NULL strings; keep existing ones even if empty */ + if (!(target->pass) && source->pass) { + target->pass = xstrdup(source->pass); + } + + if (!(target->certpath) && source->certpath) { + target->certpath = xstrdup(source->certpath); + } + + if (!(target->certfile) && source->certfile) { + target->certfile = xstrdup(source->certfile); + } + + if (!(target->certident) && source->certident) { + target->certident = xstrdup(source->certident); + } + + if (!(target->certpasswd) && source->certpasswd) { + target->certpasswd = xstrdup(source->certpasswd); + } + + if (!(target->ssl_backend) && source->ssl_backend) { + target->ssl_backend = xstrdup(source->ssl_backend); + } + + if (!(target->certhost) && source->certhost) { + target->certhost = xstrdup(source->certhost); + } + + if (target->certverify < 0 && source->certverify >= 0) { + target->certverify = source->certverify; + } + + if (target->forcessl < 0 && source->forcessl >= 0) { + target->forcessl = source->forcessl; + } + + return target; +} + +static upscli_authconf_t *upscli_add_authconf(upscli_authconf_t* node) +{ + if (!node) + return NULL; + + /* Append to end of list */ + if (!authconf_list) { + authconf_list = node; + } else { + upscli_authconf_t *tmp = authconf_list; + while (tmp->next) { + tmp = tmp->next; + } + tmp->next = node; + } + + return node; +} + +static upscli_authconf_t *upscli_add_authconf_item(const char *section) +{ + upscli_authconf_t *node = upscli_create_authconf_item(section); + + if (!node) { + fatalx(EXIT_FAILURE, "Failed to create nutauth configuration node for section '%s' which should be added to the list", NUT_STRARG(section)); + } + + return upscli_add_authconf(node); +} + +upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node) +{ + if (node) { + upscli_authconf_t *next = node->next; + + free(node->section); + free(node->user); + free(node->pass); + free(node->certpath); + free(node->certfile); + free(node->certident); + free(node->certpasswd); + free(node->ssl_backend); + free(node->certhost); + + free(node); + + return next; + } + + return NULL; +} + +static int upscli_dump_authconf_line_str(FILE *restrict stream, const char *var, const char *val, const char *indent, int for_debug) +{ + /* Assume sane inputs from upscli_dump_authconf_item(); val may be NULL */ + int res = 0; + if (!val) { + if (for_debug) { + res = fprintf(stream, + "%s%s = \n", + indent, var + ); + } + return 0; + } else { + if (for_debug == 1 && *val) { + char enc[LARGEBUF]; + res = fprintf(stream, + "%s%s = \"%s\"\n", + indent, var, pconf_encode(val, enc, sizeof(enc)) + ); + } else { + res = fprintf(stream, + "%s%s = \"%s\"\n", + indent, var, val + ); + } + } + + if (res < 0) { + upsdebugx(5, "%s: failed (%d) to effectively print %s='%s'", __func__, res, NUT_STRARG(var), NUT_STRARG(val)); + } + return res; +} + +static int upscli_dump_authconf_line_int(FILE *restrict stream, const char *var, int val, const char *indent, int for_debug) +{ + /* Assume sane inputs from upscli_dump_authconf_item(); val may be NULL */ + int res; + + /* TOTHINK: Print "-1" values when not running "for_debug"? + * We do parse them to hop over to a better preference... */ + NUT_UNUSED_VARIABLE(for_debug); + + res = fprintf(stream, + "%s%s = %d\n", + indent, var, (int)val + ); + + if (res < 0) { + upsdebugx(5, "%s: failed (%d) to effectively print %s=%d", __func__, res, NUT_STRARG(var), val); + } + return res; +} + +int upscli_dump_authconf_item(FILE *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; + + 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_str(stream, "CERTHOST", node->certhost, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_int(stream, "CERTVERIFY", node->certverify, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + res = upscli_dump_authconf_line_int(stream, "FORCESSL", node->forcessl, indent, for_debug); + if (res < 0) + return ret; + ret += res; + + return ret; +} + +size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug) +{ + upscli_authconf_t *node = authconf_list; + size_t count = 0; + + while (node) { + count++; + upscli_dump_authconf_item(stream, node, for_debug); + node = node->next; + } + + return count; +} + +void upscli_free_authconf_list(void) +{ + upscli_authconf_t *node = authconf_list; + + while (node) { + node = upscli_free_authconf_item(node); + } + + authconf_list = NULL; + current_section = NULL; + global_defaults = NULL; +} + +static void set_authconf_val(upscli_authconf_t *conf, const char *var, const char *val) +{ + if (!conf || !var) + return; + + if (!strcasecmp(var, "user")) { + if (current_section_with_fixed_username && conf->user + && (!val || (val && strcmp(conf->user, val))) + ) { + upslogx(LOG_WARNING, "USER keyword ignored for a section named like 'user@host:port'"); + return; + } + free(conf->user); + conf->user = val ? xstrdup(val) : NULL; + } else if (!strcasecmp(var, "pass") || !strcasecmp(var, "password")) { + free(conf->pass); + conf->pass = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTPATH")) { + free(conf->certpath); + conf->certpath = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTFILE")) { + free(conf->certfile); + conf->certfile = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTIDENT_NAME")) { + free(conf->certident); + conf->certident = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTIDENT_PASS")) { + free(conf->certpasswd); + conf->certpasswd = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "SSLBACKEND")) { + free(conf->ssl_backend); + conf->ssl_backend = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTHOST")) { + free(conf->certhost); + conf->certhost = val ? xstrdup(val) : NULL; + } else if (!strcmp(var, "CERTVERIFY")) { + if (val) { + if (!strcasecmp(val, "on") || !strcasecmp(val, "yes") || !strcmp(val, "1")) + conf->certverify = 1; + else if (!strcasecmp(val, "off") || !strcasecmp(val, "no") || !strcmp(val, "0")) + conf->certverify = 0; + } + } else if (!strcmp(var, "FORCESSL")) { + if (val) { + if (!strcasecmp(val, "on") || !strcasecmp(val, "yes") || !strcmp(val, "1")) + conf->forcessl = 1; + else if (!strcasecmp(val, "off") || !strcasecmp(val, "no") || !strcmp(val, "0")) + conf->forcessl = 0; + } + } else { + upslogx(LOG_WARNING, "Unrecognized authconf keyword: '%s'", var); + } +} + +static void authconf_err(const char *errmsg) +{ + upslogx(LOG_ERR, "Error in parseconf(authconf): %s", errmsg); +} + +int upscli_normalize_authconf_section_parts( + char **out_normalized_sect_name, + char **p_sect_user, + int *out_fixed_sect_user, + char **p_sect_host, + char **p_sect_port) +{ + char *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; + + /* All p_* args must be non-NULL pointers to `char *` string variables + * which may be freed and re-allocated to return normalized values + * (original strings may themselves be NULL). + * The out_* values are optional and may be NULL if you do not want + * those data points returned. + */ + if (!p_sect_user || !p_sect_host || !p_sect_port) { + upslogx(LOG_ERR, "upscli_normalize_authconf_section_parts: NULL pointer-to-string argument provided"); + return -1; + } + + /* No changes imposed here */ + sect_user = *p_sect_user; + + sect_host = *p_sect_host; + if (!sect_host || !*sect_host) { + sect_host = xstrdup("localhost"); + if (!sect_host) goto failed; + } + + sect_port = *p_sect_port; + if (sect_port && *sect_port) { + /* As port is a string, resolve it (if not a number, + * try to get one via "services" naming database) */ + char *p = sect_port; + int is_numeric = 1; + + while (*p) { + if (!isdigit((unsigned char)*p)) { + is_numeric = 0; + break; + } + p++; + } + + if (!is_numeric) { + struct servent *se = getservbyname(sect_port, "tcp"); + + if (se) { + char portbuf[16]; + if (snprintf(portbuf, sizeof(portbuf), "%u", (unsigned int)ntohs(se->s_port)) < 1) { + upsdebugx(1, "%s: Failed to construct port number from service name", __func__); + goto failed; + } + sect_port = xstrdup(portbuf); + if (!sect_port) goto failed; + } else { + upslogx(LOG_WARNING, "%s: Failed to resolve port number from service name '%s', " + "keeping original string but it is likely useless", __func__, sect_port); + } + } + } else { + char portbuf[16]; + if (snprintf(portbuf, sizeof(portbuf), "%u", (unsigned int)NUT_PORT) < 1) { + upsdebugx(1, "%s: Failed to construct default port number", __func__); + goto failed; + } + sect_port = xstrdup(portbuf); + if (!sect_port) goto failed; + } + + /* Only now that we (almost) do not expect failures, we can + * consistently populate caller's output variables (if any) */ + if (out_normalized_sect_name) { + char normalized_sect_name_buf[LARGEBUF]; + + if (snprintf(normalized_sect_name_buf, sizeof(normalized_sect_name_buf), "%s@%s:%s", + sect_user ? sect_user : "", + sect_host, + sect_port) > 0 + ) { + free(*out_normalized_sect_name); + *out_normalized_sect_name = xstrdup(normalized_sect_name_buf); + } else { + upsdebugx(1, "%s: Failed to reconstruct normalized section header", __func__); + goto failed; + } + } + + if (out_fixed_sect_user) + *out_fixed_sect_user = (sect_user && *sect_user); + + /* Different pointers? */ + if (*p_sect_host != sect_host) { + free(*p_sect_host); + *p_sect_host = sect_host; + } + + if (*p_sect_port != sect_port) { + free(*p_sect_port); + *p_sect_port = sect_port; + } + + return 0; + +failed: + free(sect_user); + free(sect_host); + free(sect_port); + + return -1; +} + +int upscli_split_authconf_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port) +{ + /* Take raw sect_name as input (e.g. a user-written string from config files). + * Normalize it by splitting into user, host, and port components (populating absent values). + * Return normalized components and reconstructed section name in output parameters (if not NULL). + */ + const char *at = NULL, *colon = NULL; + char *sect_user = NULL, *sect_host = NULL, *sect_port = NULL; + int fixed_sect_user = 0; + + if (!sect_name) { + upsdebugx(1, "%s: sect_name is NULL", __func__); + return -1; + } + + if (!(*sect_name)) { + /* TOTHINK: Should this mean `localhost@NUT_PORT`? Or global? Probably neither. */ + upsdebugx(1, "%s: sect_name is empty", __func__); + return -1; + } + + at = strchr(sect_name, '@'); + colon = strchr(sect_name, ':'); + if (at && colon && colon < at) { + upsdebugx(1, "%s: Invalid section header: colon ':' before at '@': '%s'", __func__, sect_name); + return -1; + } + + fixed_sect_user = (at && at != sect_name); + if (fixed_sect_user) { + /* If section matched user@host:port, ensure user is set to this user */ + sect_user = xstrdup(sect_name); + if (!sect_user) goto failed; + sect_user[at - sect_name] = '\0'; + } /* else keep sect_user=NULL */ + + if (at) { + if (at + 1 != colon) { + sect_host = xstrdup(at + 1); + if (!sect_host) goto failed; + if (colon) { + sect_host[colon - at - 1] = '\0'; + } + } /* else keep sect_host=NULL */ + } else { + /* No "user@" part, so just use sect_name as host - + * just because this is more likely than this being + * a specified sect_user at implicit "localhost" */ + if (sect_name + 1 != colon) { + sect_host = xstrdup(sect_name); + if (!sect_host) goto failed; + if (colon) { + sect_host[colon - sect_name] = '\0'; + } + } /* else keep sect_host=NULL */ + } + + if (colon && colon[1]) { + /* May get re-normalized below */ + sect_port = xstrdup(colon + 1); + if (!sect_port) goto failed; + } + + if (upscli_normalize_authconf_section_parts( + normalized_sect_name, + §_user, &fixed_sect_user, + §_host, §_port) < 0 + ) goto failed; + + if (out_fixed_sect_user) + *out_fixed_sect_user = fixed_sect_user; + + if (normalized_sect_user) { + *normalized_sect_user = sect_user; + } else { + free(sect_user); + } + + if (normalized_sect_host) { + *normalized_sect_host = sect_host; + } else { + free(sect_host); + } + + if (normalized_sect_port) { + *normalized_sect_port = sect_port; + } else { + free(sect_port); + } + + return 0; + +failed: + free(sect_user); + free(sect_host); + free(sect_port); + + return -1; +} + +static void handle_authconf_args(size_t numargs, char **arg, int global_scope) +{ + /* Property: var = val */ + const char *var = NULL, *val = NULL; + + if (numargs < 1) + return; + + /* Section header [section] */ + if (arg[0][0] == '[' && arg[0][strlen(arg[0])-1] == ']') { + char *sect_name = NULL, *sect_user = NULL, *sect_host = NULL, *sect_port = NULL, *normalized_sect_name = NULL; + const char *end_bracket = NULL; + upscli_authconf_t *tmp = NULL; + + if (current_section) { + upsdebugx(3, "%s: finished handling section %s", __func__, NUT_STRARG(current_section->section)); + if (current_section->section + && current_section->certhost + && *(current_section->certhost) + && upscli_split_authconf_section( + current_section->section, + &normalized_sect_name, + §_user, + ¤t_section_with_fixed_username, + §_host, §_port) >= 0 + && sect_host && *sect_host + && sect_port && *sect_port + ) { + upscli_add_host_port_cert( + sect_host, + (uint16_t)atol(sect_port), + current_section->certhost, + current_section->certverify, + current_section->forcessl); + } + } + + current_section_ignored = 0; + + sect_name = xstrdup(&arg[0][1]); /* forget leading '[' */ + end_bracket = strchr(sect_name, ']'); + if (!end_bracket || !strcmp(sect_name, "_global_defaults")) { + free(sect_name); + + if (global_scope) { + /* Subsequent lines will (re-)populate global_defaults */ + current_section = NULL; + return; + } + + current_section_ignored = 1; + upslogx(LOG_WARNING, "%s: Invalid nutauth section header format " + "in a non-global context, section contents will be ignored: %s", + __func__, arg[0]); + return; + } + + *(char *)(end_bracket) = '\0'; /* forget trailing ']' and any characters after it (comments etc.) */ + + if (upscli_split_authconf_section(sect_name, &normalized_sect_name, + §_user, ¤t_section_with_fixed_username, + §_host, §_port) < 0 + ) { + free(normalized_sect_name); + free(sect_name); + free(sect_user); + free(sect_host); + free(sect_port); + fatalx(EXIT_FAILURE, "Invalid nutauth section header: %s", NUT_STRARG(arg[0])); + } + + if (!global_scope && current_section + && (!current_section->section || strcmp(current_section->section, normalized_sect_name)) + ) { + upslogx(LOG_WARNING, "Section header [%s] ignored in included file with " + "section-scope for [%s], section contents will be ignored", + normalized_sect_name, current_section->section); + current_section_ignored = 1; + return; + } + + /* Find if section already exists */ + upsdebugx(4, "%s: Checking for existing section [%s] to import [%s]", + __func__, normalized_sect_name, sect_name); + current_section = NULL; + tmp = authconf_list; + while (tmp) { + if (tmp->section && !strcmp(tmp->section, normalized_sect_name)) { + current_section = tmp; + break; + } + tmp = tmp->next; + } + + if (!current_section) { + current_section = upscli_add_authconf_item(normalized_sect_name); + + if (current_section_with_fixed_username && sect_user && *sect_user) { + /* If section matched user@host:port, ensure + * that user field is set to this non-trivial + * value and is not modified later. */ + current_section->user = xstrdup(sect_user); + } + + /* Subsequent calls will parse lines to populate fields + * in this new section, if any; keep NULL's otherwise. + * To copy global defaults (or host defaults into an + * exact-match) to fill in the missing points, see + * upscli_get_authconf_item() for an effective complete + * momentary final configuration needed for a connection. + */ + } + + free(normalized_sect_name); + free(sect_name); + free(sect_user); + free(sect_host); + free(sect_port); + return; + } + + if (current_section_ignored) { + return; + } + + /* INCLUDE support */ + if (!strcasecmp(arg[0], "INCLUDE_REQUIRED")) { + if (numargs < 2) { + fatalx(EXIT_FAILURE, "INCLUDE_REQUIRED missing filename"); + } + + /* If we are in global scope (current_section == NULL), sub-includes are global scope. + * If we are in a section, sub-includes are section scope. + */ + parse_authconf_file(arg[1], 1, (current_section == NULL)); + return; + } + + if (!strcasecmp(arg[0], "INCLUDE")) { + if (numargs < 2) { + upslogx(LOG_ERR, "INCLUDE missing filename"); + return; + } + + /* If we are in global scope (current_section == NULL), sub-includes are global scope. + * If we are in a section, sub-includes are section scope. + */ + parse_authconf_file(arg[1], 0, (current_section == NULL)); + return; + } + + /* While above we technically also handled possible arg[0] values, + * they were not variable names - and so were not called that. */ + var = arg[0]; + if (numargs >= 3 && !strcmp(arg[1], "=")) { + val = arg[2]; + } else if (numargs == 1) { + /* Flag property? */ + val = "1"; + } + + if (current_section) { + set_authconf_val(current_section, var, val); + } else { + /* Creating/modifying global defaults */ + if (!global_defaults) { + global_defaults = upscli_add_authconf_item(NULL); + } + + set_authconf_val(global_defaults, var, val); + /* Spec says global-scope includes may modify global default items, + * as well as define new sections or overlay items in existing sections. + * My implementation handles this via current_section state. + */ + } +} + +static int parse_authconf_file(const char *filename, int fatal_errors, int global_scope) +{ + PCONF_CTX_t ctx; + + check_perms(filename); + + if (!pconf_init(&ctx, authconf_err)) { + if (fatal_errors) { + exit(EXIT_FAILURE); + } + return -1; + } + + if (!pconf_file_begin(&ctx, filename)) { + if (fatal_errors) { + fatalx(EXIT_FAILURE, "Can't open %s: %s", filename, ctx.errmsg); + } else { + upslogx(LOG_WARNING, "Can't open %s: %s", filename, ctx.errmsg); + pconf_finish(&ctx); + return -1; + } + } + + while (pconf_file_next(&ctx)) { + if (pconf_parse_error(&ctx)) { + upslogx(LOG_ERR, "Parse error: %s:%d: %s", filename, ctx.linenum, ctx.errmsg); + continue; + } + handle_authconf_args(ctx.numargs, ctx.arglist, global_scope); + } + + /* A next included file may have a different section scope, even if it has no title. + * TOTHINK: We should not reset the current_section pointer to NULL here, right? */ + current_section_ignored = 0; + + pconf_finish(&ctx); + return 1; +} + +int upscli_read_authconf_file(const char *filename, int fatal_errors) +{ + char fn[NUT_PATH_MAX + 1]; + + /* Ensure we start fresh if called multiple times */ + upscli_free_authconf_list(); + + if (!filename) { + /* Select a starting point - whichever default expected file exists; + * it may INCLUDE further files as wanted by user or site sysadmin. + */ + struct stat st; + char *s = NULL; + + s = getenv("HOME"); + if (s) { + if (snprintf(fn, sizeof(fn), "%s/.config/nut/nutauth.conf", s) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to default '%s' but it was not there", __func__, fn); + } + + if (snprintf(fn, sizeof(fn), "%s/.nutauth.conf", s) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to default '%s' but it was not there", __func__, fn); + } + } + + if (snprintf(fn, sizeof(fn), "%s/nutauth.conf", confpath()) > 0) { + if (stat(fn, &st) == 0) { + filename = fn; + goto found; + } + upsdebugx(5, "%s: tried to default '%s' but it was not there", __func__, fn); + } + +found: + if (filename) { + upsdebugx(1, "%s: defaulted to %s", __func__, filename); + } else { + if (fatal_errors) { + fatalx(EXIT_FAILURE, "Can't open a user/site-provided default nutauth.conf file"); + } else { + upslogx(LOG_WARNING, "Can't open a user/site-provided default nutauth.conf file"); + return -1; + } + } + } + + return parse_authconf_file(filename, fatal_errors, 1); +} + +upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, const char *port) +{ + upsdebugx(2, "%s: starting for [%s]@[%s]:[%s]", __func__, NUT_STRARG(user), NUT_STRARG(host), NUT_STRARG(port)); + + if (!authconf_list) { + upsdebugx(2, "%s: returning %s: no list yet", + __func__, global_defaults ? "global defaults" : "NULL"); + return global_defaults; + } + + if (!host && !port && !user) { + /* Global section only */ + if (global_defaults) { + upsdebugx(2, "%s: returning global defaults: got no specific request", __func__); + return global_defaults; + } else { + /* Should not really get here AND succeed, + * fallback just in case */ + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + if (!tmp->section || !*(tmp->section)) { + upsdebugx(2, "%s: returning the section with NULL/empty name: got no specific request", __func__); + return tmp; + } + tmp = tmp->next; + } + } + upsdebugx(2, "%s: returning NULL: no global defaults were found, nor section with NULL name: got no specific request", __func__); + return NULL; + } else { + char *sect_user = (user ? xstrdup(user) : NULL), + *sect_host = (host ? xstrdup(host) : NULL), + *sect_port = (port ? xstrdup(port) : NULL), + *normalized_sect_name = NULL; + int fixed_sect_user = 0; + upscli_authconf_t *retval = global_defaults, *tmp = NULL; + + if (upscli_normalize_authconf_section_parts( + &normalized_sect_name, + §_user, + &fixed_sect_user, + §_host, + §_port) < 0 + ) { + upsdebugx(2, "%s: returning global defaults: could not upscli_normalize_authconf_section_parts()", __func__); + goto finished; /* return default */ + } + + /* 1. Try exactly the best info we have: user@host:port (user may be or not be empty) */ + tmp = authconf_list; + while (tmp) { + upsdebugx(4, "%s: matching '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, normalized_sect_name)) { + retval = tmp; + upsdebugx(2, "%s: returning a hit of '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + goto finished; + } + tmp = tmp->next; + } + + /* 2. Retry @host:port (host defaults) if that can help? */ + if (fixed_sect_user) { + const char *target_host_port = strchr(normalized_sect_name, '@'); + + if (target_host_port[1]) { + upsdebugx(4, "%s: retry with shorter '@host:port' for host defaults (without the user part)", __func__); + + tmp = authconf_list; + while (tmp) { + upsdebugx(4, "%s: matching '%s' against '%s'", __func__, target_host_port, NUT_STRARG(tmp->section)); + if (tmp->section && !strcmp(tmp->section, target_host_port)) { + retval = tmp; + upsdebugx(2, "%s: returning a hit of '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + goto finished; + } + tmp = tmp->next; + } + } + } + + /* 3. Global defaults (section == NULL) */ + upsdebugx(2, "%s: returning global defaults: no more specific hit was found", __func__); + retval = global_defaults; + +finished: + free(sect_user); + free(sect_host); + free(sect_port); + free(normalized_sect_name); + return retval; + } +} + +upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, const char *port, int add_to_list) +{ + upscli_authconf_t *retval = global_defaults, *retval_user = NULL, *retval_host = NULL; + char *sect_user = (user ? xstrdup(user) : NULL), + *sect_host = (host ? xstrdup(host) : NULL), + *sect_port = (port ? xstrdup(port) : NULL), + *normalized_sect_name = NULL; + int fixed_sect_user = 0, created_item = 0; + + upsdebugx(2, "%s: starting for [%s]@[%s]:[%s]", __func__, NUT_STRARG(user), NUT_STRARG(host), NUT_STRARG(port)); + + /* We want this parsed always, so we can know if there + * is a fixed user, or assign the section name, at least */ + if (upscli_normalize_authconf_section_parts( + &normalized_sect_name, + §_user, + &fixed_sect_user, + §_host, + §_port) < 0 + ) { + upsdebugx(2, "%s: could not upscli_normalize_authconf_section_parts()", __func__); + } + upsdebugx(4, "%s: after normalization, proceeding for [%s]@[%s]:[%s] => '%s' (with%s fixed USER part)", + __func__, NUT_STRARG(sect_user), NUT_STRARG(sect_host), NUT_STRARG(sect_port), + NUT_STRARG(normalized_sect_name), fixed_sect_user ? "" : "out"); + + if (!authconf_list) { + upsdebugx(4, "%s: best match is %s: no list yet", + __func__, global_defaults ? "global defaults" : "NULL"); + goto found; + } + + if (!host && !port && !user) { + /* Global section only */ + if (global_defaults) { + upsdebugx(4, "%s: best match is global defaults: got no specific request", __func__); + goto found; + } else { + /* Should not really get here AND succeed, + * fallback just in case */ + upscli_authconf_t *tmp = authconf_list; + while (tmp) { + if (!tmp->section || !*(tmp->section)) { + upsdebugx(4, "%s: best match is the section with NULL/empty name: got no specific request", __func__); + goto found; + } + tmp = tmp->next; + } + } + upsdebugx(4, "%s: best match is NULL: no global defaults were found, nor section with NULL name: got no specific request", __func__); + goto found; + } else { + const char *at = (fixed_sect_user ? strchr(normalized_sect_name, '@') : NULL); + upscli_authconf_t *tmp = authconf_list; + + while (tmp) { + upsdebugx(4, "%s: matching '%s' against '%s'", __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (tmp->section) { + if (!strcmp(tmp->section, normalized_sect_name)) { + if (fixed_sect_user) { + /* normalized_sect_name is user@host:port */ + retval_user = tmp; + upsdebugx(2, "%s: got exact user+host+port hit of '%s' against '%s'", + __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + if (retval_host) + break; + } else { + /* normalized_sect_name is @host:port */ + retval_host = tmp; + upsdebugx(2, "%s: got host+port hit of '%s' against '%s'", + __func__, normalized_sect_name, NUT_STRARG(tmp->section)); + break; + } + } else + if (fixed_sect_user && !strcmp(tmp->section, at)) { + /* normalized_sect_name is user@host:port and we match '@host:port' */ + retval_host = tmp; + upsdebugx(2, "%s: got host+port hit of '%s' against '%s'", + __func__, at, NUT_STRARG(tmp->section)); + } + } + tmp = tmp->next; + } + + if (retval_user) { + retval = retval_user; + } else + if (retval_host) { + if (fixed_sect_user) + upsdebugx(4, "%s: did not find an exact user+host+port match in the list, only host+port", __func__); + retval = retval_host; + } else { + /* keep global_defaults or NULL, handle below */ + upsdebugx(4, "%s: did not find a match in the list", __func__); + } + } + +found: + if (!retval || retval == global_defaults) { + upsdebugx(2, "%s: best match from the list is %s", + __func__, global_defaults ? "global defaults" : "NULL"); + if (!global_defaults) { + upsdebugx(3, "%s: create new item for section '%s'", + __func__, normalized_sect_name); + retval = upscli_create_authconf_item(normalized_sect_name); + created_item = 1; + } else { + upsdebugx(3, "%s: clone new item for section '%s' from global_defaults", + __func__, normalized_sect_name); + retval = upscli_clone_authconf_item(global_defaults, normalized_sect_name); + created_item = 1; + } + } else { + if (!add_to_list || (!retval_user && fixed_sect_user)) { + upsdebugx(3, "%s: clone new item for section '%s' from '%s'", + __func__, normalized_sect_name, NUT_STRARG(retval->section)); + retval = upscli_clone_authconf_item(retval, normalized_sect_name); + created_item = 1; + } + } + + if (fixed_sect_user) { + free(retval->user); + retval->user = xstrdup(user); + } + + if (retval_user && retval_host) { + /* our retval is (maybe a clone of) retval_user */ +#ifdef DEBUG + upsdebugx(1, "merge user="); upscli_dump_authconf_item(NULL, retval, 1); + upsdebugx(1, "...and host="); upscli_dump_authconf_item(NULL, retval_host, 1); +#endif + upscli_merge_authconf_item(retval_host, retval); + } + + if ((retval_user || retval_host) && global_defaults) { + /* our retval is (maybe a clone of) retval_user or retval_host */ +#ifdef DEBUG + upsdebugx(1, "merge user/host="); upscli_dump_authconf_item(NULL, retval, 1); + upsdebugx(1, "...and globaldef="); upscli_dump_authconf_item(NULL, global_defaults, 1); +#endif + upscli_merge_authconf_item(global_defaults, retval); + } + +#ifdef DEBUG + upsdebugx(1, "final state ="); upscli_dump_authconf_item(NULL, retval, 1); +#endif + + if (add_to_list) { + if (created_item) { + upsdebugx(4, "%s: adding result to list", __func__); + upscli_add_authconf(retval); + } else { + upsdebugx(4, "%s: not adding result to list: edited existing item in-place", __func__); + } + } else { + upsdebugx(4, "%s: not adding result to list, caller must free it eventually", __func__); + } + + free(sect_user); + free(sect_host); + free(sect_port); + free(normalized_sect_name); + + return retval; +} diff --git a/clients/authconf.h b/clients/authconf.h new file mode 100644 index 0000000000..f846d304a0 --- /dev/null +++ b/clients/authconf.h @@ -0,0 +1,124 @@ +/* authconf.h - prototypes and structures for NUT client authentication configuration + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef NUT_AUTHCONF_H_SEEN +#define NUT_AUTHCONF_H_SEEN 1 + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "nut_stdint.h" + +typedef struct upscli_authconf_s { + char *section; /* [@host:port] or [user@host:port], or NULL for global */ + char *user; + char *pass; + char *certpath; + char *certfile; + char *certident; + char *certpasswd; /* Password for key/cert storage */ + char *ssl_backend; /* openssl/nss */ + char *certhost; + int certverify; /* -1 = unset, 0 = off, 1 = on */ + int forcessl; /* -1 = unset, 0 = off, 1 = on */ + + struct upscli_authconf_s *next; +} upscli_authconf_t; + +/** Get the one global list of all parsed authentication configurations */ +upscli_authconf_t *upscli_get_authconf_list(void); + +/** Create a one-off configuration item, upscli_free_authconf_item() it manually */ +upscli_authconf_t *upscli_create_authconf_item(const char *section); + +/** Create a one-off configuration item, upscli_free_authconf_item() it manually */ +upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const char *section); + +/** Merge contents of two existing configuration items, they may be or not be on the list */ +upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_authconf_t *target); + +/** Free an authentication configuration item (if not NULL) and return its "next" pointer */ +upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node); + +/** Free the list of authentication configurations */ +void upscli_free_authconf_list(void); + +/** Read the authentication configuration file (usually nutauth.conf) + * If filename==NULL, tries to locate per-user ${HOME}/.config/nut/nutauth.conf + * and ${HOME}/.nutauth.conf, or site default ${nutconfdir}/nutauth.conf + * (whichever is found first); then one can follow `INCLUDE` trail if needed. + * Returns -1 on error, 1 on success + */ +int upscli_read_authconf_file(const char *filename, int fatal_errors); + +/** All p_* args must be non-NULL pointers to `char *` string variables + * which may be freed and re-allocated to return normalized values + * (original strings may themselves be NULL). + * The out_* values are optional and may be NULL if you do not want + * those data points returned. + */ +int upscli_normalize_authconf_section_parts( + char **out_normalized_sect_name, + char **p_sect_user, + int *out_fixed_sect_user, + char **p_sect_host, + char **p_sect_port); + +/** Take raw sect_name as input (e.g. a user-written string from config files). + * Normalize it by splitting into user, host, and port components (populating absent values). + * Return normalized components and reconstructed section name in output parameters (if not NULL), + * and 0 for successful completion or -1 if any error happened along the way. + */ +int upscli_split_authconf_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port); + +/** Find the best matching authconf for a given connection string in the list; + * if all args are NULL, return the global section or NULL if none such in the list. + */ +upscli_authconf_t *upscli_find_authconf_item(const char *user, const char *host, const char *port); + +/** Find the best matching authconf for a given connection string, and fill in + * the missing points from higher levels (exact match => host defaults => global). + * Based on `add_to_list` flag, the returned item is always new and unique and + * not on the list (can adapt to changes in higher levels but must be freed by + * caller), or will be edited on or added to the list (subsequent calls would + * likely not add anything new, but memory management is easier, data is cached). + * if all args are NULL, return the global section or NULL if none such in the list. + */ +upscli_authconf_t *upscli_get_authconf_item(const char *user, const char *host, const char *port, int add_to_list); + +/** Print one node to the specified stream (stdout if NULL), + * return code similar to fprintf() - sum of printed characters. + * + * The for_debug value controls the verbosity of the output: + * 0 - do not print NULL strings, do not indent global section + * 1 - print strings, indent global [] section as any other + * 2 - like 1, but do not escape special characters in strings (only double-quote them). + * + * Used from upscli_dump_authconf_list() */ +int upscli_dump_authconf_item(FILE *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 */ +size_t upscli_dump_authconf_list(FILE *restrict stream, int for_debug); + +#ifdef __cplusplus +} +#endif + +#endif /* NUT_AUTHCONF_H_SEEN */ diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 1e85779db3..27b416a615 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2571,23 +2571,28 @@ SSLConfig_CERTHOST::SSLConfig_CERTHOST( const std::string& host_addr, const std::string& cert_subj, int forcessl, - int certverify) + int certverify, + uint16_t port) : _host_addr(host_addr), _cert_subj(cert_subj), _forcessl(forcessl), - _certverify(certverify) + _certverify(certverify), + _port(port) { + // TODO: Parse apart possible "host:port" spelling, involve getservbyname() } SSLConfig_CERTHOST::SSLConfig_CERTHOST( const char *host_addr, const char *cert_subj, int forcessl, - int certverify) + int certverify, + uint16_t port) : _host_addr(host_addr ? host_addr : ""), _cert_subj(cert_subj ? cert_subj : ""), _forcessl(forcessl), - _certverify(certverify) + _certverify(certverify), + _port(port) { } @@ -2610,6 +2615,11 @@ const char *SSLConfig_CERTHOST::getHostAddr_c_str() const return _host_addr.empty() ? nullptr : _host_addr.c_str(); } +uint16_t SSLConfig_CERTHOST::getPort() const +{ + return _port; +} + const std::string& SSLConfig_CERTHOST::getCertSubj() const { return _cert_subj; @@ -2632,7 +2642,10 @@ int SSLConfig_CERTHOST::getCertVerify() const bool SSLConfig_CERTHOST::operator < (const SSLConfig_CERTHOST& other) const { - if (_cert_subj.empty() && other._cert_subj.empty()) return _host_addr < other._host_addr; + if (_cert_subj.empty() && other._cert_subj.empty()) { + if (_host_addr == other._host_addr) return _port < other._port; + return _host_addr < other._host_addr; + } return _cert_subj < other._cert_subj; } @@ -2811,17 +2824,17 @@ const SSLConfig_CERTHOST *SSLConfig::getFirstCertHost() const return _certhosts.empty() ? nullptr : *(_certhosts.begin()); } -const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddr(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddr(const std::string &s, uint16_t port) const { for (const auto* item : _certhosts) { - if (item->getHostAddr() == s) { + if (item->getHostAddr() == s && (port == 0 || item->getPort() == 0 || item->getPort() == port)) { return item; } } return nullptr; } -const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(const std::string &s) const { for (const auto* item : _certhosts) { if (item->getCertSubj() == s) { @@ -2831,10 +2844,10 @@ const SSLConfig_CERTHOST *SSLConfig::getCertHostBySubj(std::string &s) const return nullptr; } -const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddrOrSubj(std::string &s) const +const SSLConfig_CERTHOST *SSLConfig::getCertHostByAddrOrSubj(const std::string &s, uint16_t port) const { for (const auto* item : _certhosts) { - if (item->getHostAddr() == s || item->getCertSubj() == s) { + if ((item->getHostAddr() == s && (port == 0 || item->getPort() == 0 || item->getPort() == port)) || item->getCertSubj() == s) { return item; } } diff --git a/clients/nutclient.h b/clients/nutclient.h index e1c544aae9..e73ee53a57 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -402,17 +402,20 @@ class SSLConfig_CERTIDENT_NSS : public SSLConfig_CERTIDENT class SSLConfig_CERTHOST { public: + /** NOTE: Addr would be parsed into host:port and a 0 port may become NUT_PORT */ SSLConfig_CERTHOST( const std::string& host_addr, const std::string& cert_subj, int forcessl = -1, - int certverify = -1); + int certverify = -1, + uint16_t port = 0); SSLConfig_CERTHOST( const char *host_addr, const char *cert_subj, int forcessl = -1, - int certverify = -1); + int certverify = -1, + uint16_t port = 0); SSLConfig_CERTHOST& operator=(const SSLConfig_CERTHOST&) = default; SSLConfig_CERTHOST(const SSLConfig_CERTHOST&) = default; @@ -424,6 +427,8 @@ class SSLConfig_CERTHOST const std::string& getHostAddr() const; const char *getHostAddr_c_str() const; + uint16_t getPort() const; + const std::string& getCertSubj() const; const char *getCertSubj_c_str() const; @@ -438,6 +443,7 @@ class SSLConfig_CERTHOST std::string _cert_subj; int _forcessl; int _certverify; + uint16_t _port; }; /** @@ -496,9 +502,10 @@ class SSLConfig /** Simplify workflow for single-server connections */ const SSLConfig_CERTHOST *getFirstCertHost() const; - const SSLConfig_CERTHOST *getCertHostByAddr(std::string &s) const; - const SSLConfig_CERTHOST *getCertHostBySubj(std::string &s) const; - const SSLConfig_CERTHOST *getCertHostByAddrOrSubj(std::string &s) const; + /** NOTE: Addr would be parsed into host:port and a 0 port may become NUT_PORT */ + const SSLConfig_CERTHOST *getCertHostByAddr(const std::string &s, uint16_t port = 0) const; + const SSLConfig_CERTHOST *getCertHostBySubj(const std::string &s) const; + const SSLConfig_CERTHOST *getCertHostByAddrOrSubj(const std::string &s, uint16_t port = 0) const; /** Callback to apply this configuration into a TcpClient instance * (and further propagate into a Socket instance used by it). diff --git a/clients/upsclient.c b/clients/upsclient.c index ed7ba58515..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; } @@ -757,6 +762,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_port_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) @@ -1072,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); @@ -1091,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; @@ -1105,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 @@ -1669,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 @@ -1879,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); @@ -2154,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 7be5c8b537..4f47c9151b 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -69,6 +69,7 @@ extern "C" { #define UPSCLI_NETBUF_LEN 512 /* network i/o buffer */ #include "parseconf.h" +#include "authconf.h" #ifdef WITH_OPENSSL /* Adapted from https://linux.die.net/man/3/ssl_set_verify man page example */ @@ -152,12 +153,15 @@ struct timeval *upscli_upslog_start_sync(struct timeval *tv, const void *cookie) * client certificate file. Equivalent to prefer upscli_init2(..., NULL) */ int upscli_init(int certverify, const char *certpath, const char *certname, const char *certpasswd); int upscli_init2(int certverify, const char *certpath, const char *certname, const char *certpasswd, const char *certfile); +int upscli_init_authconf(upscli_authconf_t *ac); int upscli_cleanup(void); int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags, struct timeval *tv); /* blocking unless default timeout is specified, see also: upscli_init_default_connect_timeout() */ int upscli_connect(UPSCONN_t *ups, const char *host, uint16_t port, int flags); +void upscli_add_host_port_cert(const char* hostname, uint16_t port, const char* certname, int certverify, int forcessl); +/* hostname may be a host:port */ void upscli_add_host_cert(const char* hostname, const char* certname, int certverify, int forcessl); /* --- functions that only use the new names --- */ diff --git a/common/common.c b/common/common.c index ef9e68f71e..014b55b73d 100644 --- a/common/common.c +++ b/common/common.c @@ -43,9 +43,7 @@ #endif #include -#if !HAVE_DECL_REALPATH -# include -#endif +#include /* Just yield a unique value - e.g. address of a statically allocated variable * which would be different if several copies of NUT-common object code are @@ -613,6 +611,28 @@ pid_t get_max_pid_t(void) #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE #pragma GCC diagnostic pop #endif +} + +void check_perms(const char *fn) +{ +#ifndef WIN32 + int ret; + struct stat st; + + ret = stat(fn, &st); + + if (ret != 0) { + fatal_with_errno(EXIT_FAILURE, "stat %s", fn); + } + + /* include the x bit here in case we check a directory */ + if (st.st_mode & (S_IROTH | S_IXOTH)) { + upslogx(LOG_WARNING, "WARNING: %s is world readable (hope you don't have passwords there)", fn); + } +#else /* WIN32 */ + NUT_UNUSED_VARIABLE(fn); + NUT_WIN32_INCOMPLETE_MAYBE_NOT_APPLICABLE(); +#endif /* WIN32 */ } /* Normally sendsignalfn(), sendsignalpid() and related methods call @@ -5418,7 +5438,7 @@ void nut_prepare_search_paths(void) { upsdebugx(5, "%s: SKIP " "unreachable directory #%" PRIuSIZE " : %s", __func__, index, NUT_STRARG(dirname)); - index++; + index++; continue; } index++; diff --git a/conf/.gitignore b/conf/.gitignore index fb8a2fe90d..de6ede8acf 100644 --- a/conf/.gitignore +++ b/conf/.gitignore @@ -1,3 +1,4 @@ +/nutauth.conf.sample /upsmon.conf.sample /upssched.conf.sample /upsstats-single.html.sample diff --git a/conf/Makefile.am b/conf/Makefile.am index a3f49f9a6a..b8b9f4f6f8 100644 --- a/conf/Makefile.am +++ b/conf/Makefile.am @@ -7,7 +7,7 @@ INSTALL_0600 = $(INSTALL) -m 0600 # Note: ups.conf is a secured file, because for networked devices # it can contain SNMP, NetXML, IPMI or similar credentials SECFILES_STATIC = upsd.conf.sample upsd.users.sample ups.conf.sample -SECFILES_GENERATED = upsmon.conf.sample +SECFILES_GENERATED = upsmon.conf.sample nutauth.conf.sample PUBFILES_STATIC = nut.conf.sample PUBFILES_GENERATED = upssched.conf.sample CGIPUB_STATIC = hosts.conf.sample upsset.conf.sample @@ -31,7 +31,7 @@ nodist_conf_examples_DATA = $(SECFILES_GENERATED) $(PUBFILES_GENERATED) \ $(CGI_INSTALL_GENERATED) SPELLCHECK_SRC = $(dist_sysconf_DATA) \ - upssched.conf.sample.in upsmon.conf.sample.in \ + nutauth.conf.sample.in upssched.conf.sample.in upsmon.conf.sample.in \ upsstats-modern-list.html.sample.in upsstats-modern-single.html.sample.in \ upsstats.html.sample.in upsstats-single.html.sample.in @@ -55,6 +55,6 @@ SPELLCHECK_SRC = $(dist_sysconf_DATA) \ spellcheck spellcheck-interactive spellcheck-sortdict: @dotMAKE@ +$(MAKE) -f $(top_builddir)/docs/Makefile $(AM_MAKEFLAGS) MKDIR_P="$(MKDIR_P)" builddir="$(builddir)" srcdir="$(srcdir)" top_builddir="$(top_builddir)" top_srcdir="$(top_srcdir)" SPELLCHECK_SRC="$(SPELLCHECK_SRC)" SPELLCHECK_SRCDIR="$(srcdir)" SPELLCHECK_BUILDDIR="$(builddir)" $@ - +# WARNING: Do not clean away files generated from templates by configure script! MAINTAINERCLEANFILES = Makefile.in .dirstamp CLEANFILES = *.pdf *.html *-spellchecked diff --git a/conf/nutauth.conf.sample.in b/conf/nutauth.conf.sample.in new file mode 100644 index 0000000000..9edc37125f --- /dev/null +++ b/conf/nutauth.conf.sample.in @@ -0,0 +1,43 @@ +# 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!" +# CERTVERIFY = 1 +# 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 028db966c1..55a52a880e 100644 --- a/configure.ac +++ b/configure.ac @@ -7282,6 +7282,7 @@ AC_CONFIG_FILES([ clients/Makefile common/Makefile conf/Makefile + conf/nutauth.conf.sample conf/upsmon.conf.sample conf/upssched.conf.sample conf/upsstats.html.sample diff --git a/docs/Makefile.am b/docs/Makefile.am index cb583a0654..f33a9d6d53 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -865,6 +865,11 @@ SPELLCHECK_ENV_DEBUG = no ASPELL_NUT_COMMON_ARGS = -p $(abs_srcdir)/$(NUT_SPELL_DICT) ASPELL_NUT_COMMON_ARGS += -d en --lang=en --ignore-accents ASPELL_NUT_COMMON_ARGS += --encoding=utf-8 +# Do not let it remove words it thinks are not needed (in interactive mode) -- +# default dictionaries differ from platform to platform, from decade to decade: +ASPELL_NUT_COMMON_ARGS += --dont-validate-words +ASPELL_NUT_COMMON_ARGS += --dont-clean-words +ASPELL_NUT_COMMON_ARGS += --dont-clean-affixes # Note: If there is a need to use filter path (e.g. in mingw/msys2 builds), # it must be before --mode=tex (-t) option! ASPELL_NUT_TEXMODE_ARGS = diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index abe8bd0e22..ac625f51b4 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -245,6 +245,7 @@ check-list-SRC_ALL_PAGES: # Base configuration and client manpages, always installed SRC_CONF_PAGES = \ nut.conf.txt \ + nutauth.conf.txt \ ups.conf.txt \ upsd.conf.txt \ upsd.users.txt \ @@ -253,6 +254,7 @@ SRC_CONF_PAGES = \ INST_MAN_CONF_PAGES = \ nut.conf.$(MAN_SECTION_CFG) \ + nutauth.conf.$(MAN_SECTION_CFG) \ ups.conf.$(MAN_SECTION_CFG) \ upsd.conf.$(MAN_SECTION_CFG) \ upsd.users.$(MAN_SECTION_CFG) \ @@ -269,6 +271,7 @@ mancfg_DATA += $(MAN_CONF_PAGES) INST_HTML_CONF_MANS = \ nut.conf.html \ + nutauth.conf.html \ ups.conf.html \ upsd.conf.html \ upsd.users.html \ @@ -553,6 +556,12 @@ SRC_DEV_PAGES = \ upscli_strerror.txt \ upscli_upserror.txt \ upscli_upslog_set_debug_level.txt \ + upscli_create_authconf_item.txt \ + upscli_dump_authconf_item.txt \ + upscli_find_authconf_item.txt \ + upscli_free_authconf_list.txt \ + upscli_get_authconf_list.txt \ + upscli_read_authconf_file.txt \ upscli_str_add_unique_token.txt \ upscli_str_contains_token.txt \ libnutclient.txt \ @@ -721,6 +730,7 @@ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS): upscli_upslog_set_debug_level.$(MAN_SECTI INST_MAN_DEV_API_PAGES = \ upsclient.$(MAN_SECTION_API) \ upscli_add_host_cert.$(MAN_SECTION_API) \ + $(UPSCLI_ADD_HOST_CERT_DEPS) \ upscli_cleanup.$(MAN_SECTION_API) \ upscli_connect.$(MAN_SECTION_API) \ upscli_tryconnect.$(MAN_SECTION_API) \ @@ -749,6 +759,15 @@ INST_MAN_DEV_API_PAGES = \ upscli_strerror.$(MAN_SECTION_API) \ upscli_upserror.$(MAN_SECTION_API) \ upscli_upslog_set_debug_level.$(MAN_SECTION_API) \ + upscli_create_authconf_item.$(MAN_SECTION_API) \ + $(UPSCLI_CREATE_AUTHCONF_DEPS) \ + upscli_dump_authconf_item.$(MAN_SECTION_API) \ + $(UPSCLI_DUMP_AUTHCONF_DEPS) \ + upscli_find_authconf_item.$(MAN_SECTION_API) \ + $(UPSCLI_FIND_AUTHCONF_DEPS) \ + upscli_free_authconf_list.$(MAN_SECTION_API) \ + upscli_get_authconf_list.$(MAN_SECTION_API) \ + upscli_read_authconf_file.$(MAN_SECTION_API) \ $(UPSCLI_UPSLOG_SET_DEBUG_LEVEL_DEPS) \ upscli_str_add_unique_token.$(MAN_SECTION_API) \ upscli_str_contains_token.$(MAN_SECTION_API) \ @@ -797,10 +816,14 @@ INST_MAN_DEV_API_PAGES = \ nutscan_init.$(MAN_SECTION_API) # Alias page for one text describing two commands: -UPSCLI_INIT_DEPS = upscli_init2.$(MAN_SECTION_API) +UPSCLI_INIT_DEPS = upscli_init2.$(MAN_SECTION_API) upscli_init_authconf.$(MAN_SECTION_API) $(UPSCLI_INIT_DEPS): upscli_init.$(MAN_SECTION_API) touch $@ +UPSCLI_ADD_HOST_CERT_DEPS = upscli_add_host_port_cert.$(MAN_SECTION_API) +$(UPSCLI_ADD_HOST_CERT_DEPS): upscli_add_host_cert.$(MAN_SECTION_API) + touch $@ + upscli_readline_timeout.$(MAN_SECTION_API): upscli_readline.$(MAN_SECTION_API) touch $@ @@ -816,6 +839,27 @@ upscli_sendline_timeout_may_disconnect.$(MAN_SECTION_API): upscli_sendline.$(MAN upscli_tryconnect.$(MAN_SECTION_API): upscli_connect.$(MAN_SECTION_API) touch $@ +UPSCLI_CREATE_AUTHCONF_DEPS = \ + upscli_clone_authconf_item.$(MAN_SECTION_API) \ + upscli_merge_authconf_item.$(MAN_SECTION_API) \ + upscli_free_authconf_item.$(MAN_SECTION_API) + +UPSCLI_DUMP_AUTHCONF_DEPS = upscli_dump_authconf_list.$(MAN_SECTION_API) + +UPSCLI_FIND_AUTHCONF_DEPS = \ + upscli_get_authconf_item.$(MAN_SECTION_API) \ + upscli_normalize_authconf_section_parts.$(MAN_SECTION_API) \ + upscli_split_authconf_section.$(MAN_SECTION_API) + +$(UPSCLI_CREATE_AUTHCONF_DEPS): upscli_create_authconf_item.$(MAN_SECTION_API) + touch $@ + +$(UPSCLI_DUMP_AUTHCONF_DEPS): upscli_dump_authconf_item.$(MAN_SECTION_API) + touch $@ + +$(UPSCLI_FIND_AUTHCONF_DEPS): upscli_find_authconf_item.$(MAN_SECTION_API) + touch $@ + nutscan_scan_ip_range_snmp.$(MAN_SECTION_API): nutscan_scan_snmp.$(MAN_SECTION_API) touch $@ @@ -890,6 +934,12 @@ INST_HTML_DEV_MANS = \ upscli_strerror.html \ upscli_upserror.html \ upscli_upslog_set_debug_level.html \ + upscli_create_authconf_item.html \ + upscli_dump_authconf_item.html \ + upscli_find_authconf_item.html \ + upscli_free_authconf_list.html \ + upscli_get_authconf_list.html \ + upscli_read_authconf_file.html \ upscli_str_add_unique_token.html \ upscli_str_contains_token.html \ libnutclient.html \ @@ -957,6 +1007,21 @@ upscli_sendline_timeout.html upscli_sendline_timeout_may_disconnect.html: upscli upscli_tryconnect.html: upscli_connect.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ +upscli_get_authconf_item.html: upscli_find_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_normalize_authconf_section_parts.html: upscli_find_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_split_authconf_section.html: upscli_find_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_free_authconf_item.html: upscli_create_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + +upscli_dump_authconf_list.html: upscli_dump_authconf_item.html + test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ + nutscan_scan_ip_range_snmp.html: nutscan_scan_snmp.html test -n '$?' -a -s '$@' && rm -f $@ && ln -s $? $@ diff --git a/docs/man/nutauth.conf.txt b/docs/man/nutauth.conf.txt new file mode 100644 index 0000000000..1b70835dfa --- /dev/null +++ b/docs/man/nutauth.conf.txt @@ -0,0 +1,196 @@ +NUTAUTH.CONF(5) +=============== + +NAME +---- + +nutauth.conf - Authentication and SSL configuration for NUT clients + +DESCRIPTION +----------- + +This file is read by the NUT client library linkman:libupsclient[3] via +linkman:upscli_read_authconf_file[3]. It allows users to define default and +per-server authentication credentials (username and password) and SSL/TLS +settings (certificates, verification, etc.) for use by NUT clients like +linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], and others. +Note that there is a dedicated linkman:upsmon.conf[5] configuration file +for the linkman:upsmon[8] client. + +This file begins with optional global directives which can provide defaults +for all connections. Per-server or per-account sections can then be defined +to override these defaults. + +SECTION TITLES +~~~~~~~~~~~~~~ + +A section begins with a name in brackets. Sections are matched against the +server being contacted. Supported section formats are: + +*[@host:port]*:: + Defines defaults for a specific host and port. + +*[@host]*:: + Defines defaults for a specific host (uses the default NUT port + as defined at build configuration time, implicit default is `3493`). + +*[user@host:port]*:: + Defines credentials and settings for a specific user on a specific + host and port. The `USER` directive would be ignored in this entry. + +*[user@host]*:: + Defines credentials and settings for a specific user on a specific + host (uses the default NUT port). + +*[host]*:: + A section title without an `@` character effectively defines defaults + for a specific host and default NUT port, just because this is more + likely than such a string being a specified "user" at an implicit + "localhost". ++ +Consequently, titles with a colon `:` without an `@` are interpreted + as *[host:port]* (or *[:port]* with an implicit "localhost"). + +Section titles are normalized when parsing the `nutauth.conf` file(s), +so the inclusion order (nesting) and run-time search for best-match +configuration can be deterministic. + +For example: + +* an empty 'host' component value is interpreted as `localhost`, e.g. + in `[@:port]` spelling; +* a non-numeric 'port' string would be resolved in the OS service naming + database, if possible, e.g. in `[@localhost:nut]` spelling; +* an empty or absent 'port' component is understood as using the default + NUT port (as detailed above for `[@host]` spelling). + +A section continues until the next section header or until EOF. + +The special names `[]` and `[_global_defaults]` are reserved and should +not normally be used as a section header; just use the global scope (lines +before any section header) for default settings. These reserved explicit +names can however be helpful to maintain order in nested files that you +can `INCLUDE` in your top-level `nutauth.conf`. + +Note that lines which seem like the global scope in an included configuration +(not preceded by any section title) would modify the parent section; however +lines after an explicit section title -- even the `[_global_defaults]` +one -- would not. + +OTHER FORMAT CONSIDERATIONS +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuration directives are specified as `KEY = VALUE` pairs; simple string +values may be passed "as is", but those with spaces or other special characters +should be double-quoted and/or use escape sequences, like in other NUT files. + +Blank lines and characters after an un-quoted hash (`#`) are ignored. + +Example: + + # Global defaults + CERTVERIFY = on + FORCESSL = on + + # Host defaults for a local server + [@localhost] + PASS = "password1" + + # Specific account on a remote server + [admin@remoteserver:3493] + PASS = "secret2" + CERTVERIFY = off + +IMPORTANT NOTES +--------------- + +* Contents of this file should be pure ASCII. +* Permissions should be restricted (e.g., `chmod 600`) since this file + contains passwords. + +GLOBAL AND SECTION DIRECTIVES +----------------------------- + +The following keywords are supported in both global scope and within sections: + +*user* (case-insensitive token):: + Optional. Specify the NUT username for authentication (as defined by + linkman:upsd.users[5] on the data server side). If the section header + already specified a user (e.g., `[user@host]`), this keyword is ignored + within that section. + +*pass* or *password* (case-insensitive token):: + Optional. Specify the NUT password for authentication. + +*CERTPATH*:: + Optional. Specify the path to trusted CA certificates (e.g., for + verifying the server's certificate), otherwise the system default + CA certificate store is used. In case of NSS, this is the path to + location of the NSS DB files used for all purposes. + +*CERTFILE*:: + Optional (OpenSSL only). Specify the client certificate file for + client-side authentication to the server. The PEM file should start + with the individual certificate, followed by the chain of certificates + of authorities that issued it, and finished by the private key. + +*CERTIDENT_NAME*:: + Optional. Specify the client certificate identity (nickname, alias). + +*CERTIDENT_PASS*:: + Optional. Specify the password to decrypt the client's private key. + +*SSLBACKEND*:: + Optional. Specify the SSL/TLS backend to use (e.g., `openssl` or `nss`), + if the default is not suitable. + +*CERTHOST*:: + Optional. Specify the expected certificate subject (common name) of that + server's certificate; alternately the IP address or host name used in + the section title should match that in the common name (CN) or subject + alternate names (SAN). + +*CERTVERIFY*:: + Optional. Enable or disable server certificate verification. + Accepted values: `on`, `yes`, `1` (enable) or `off`, `no`, `0` (disable). + +*FORCESSL*:: + Optional. Require SSL/TLS for the connection. + Accepted values: `on`, `yes`, `1` (enable) or `off`, `no`, `0` (disable). + +NESTING (INCLUDE FILES) +----------------------- + +Included files are supported via the `INCLUDE` directive for optionally +present files, and `INCLUDE_REQUIRED` for files that must be there +(otherwise the program exits with a fatal error). + +Global-scope includes may modify global default items, as well as define +new sections or overlay items in existing sections. + +Section-scope includes (nested within a section) can only modify data +within that section. They do not require a section title (effectively the +global section of the included file modifies the section of the parent +file it was included into), but if these files do have any section title(s), +then contents of sections that after normalization do not match the section +title of the parent file would be skipped. + +Example: + + INCLUDE_REQUIRED /etc/nut/nutauth-defaults.conf + + [@localhost] + INCLUDE /etc/nut/nutauth-local.conf + +SEE ALSO +-------- + +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upsc[8], linkman:upsrw[8], linkman:upscmd[8], +linkman:upsd.users[5], +linkman:upsmon.conf[5], linkman:upsmon[8] + +Internet resources +~~~~~~~~~~~~~~~~~~ + +The NUT (Network UPS Tools) home page: https://www.networkupstools.org/ diff --git a/docs/man/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 diff --git a/docs/man/upscli_create_authconf_item.txt b/docs/man/upscli_create_authconf_item.txt new file mode 100644 index 0000000000..2040441c74 --- /dev/null +++ b/docs/man/upscli_create_authconf_item.txt @@ -0,0 +1,97 @@ +UPSCLI_CREATE_AUTHCONF_ITEM(3) +============================== + +NAME +---- + +upscli_create_authconf_item, upscli_clone_authconf_item, +upscli_merge_authconf_item, upscli_free_authconf_item - Create +or free an individual authentication configuration item, +which is not necessarily in the globally tracked list + +SYNOPSIS +-------- + +------ + #include + + typedef struct upscli_authconf_s { + char *section; /* [@host:port] or [user@host:port], or NULL for global */ + char *user; + char *pass; + char *certpath; + char *certfile; + char *certident; + char *certpasswd; /* Password for key/cert storage */ + char *ssl_backend; /* openssl/nss */ + char *certhost; + int certverify; /* -1 = unset, 0 = off, 1 = on */ + int forcessl; /* -1 = unset, 0 = off, 1 = on */ + + struct upscli_authconf_s *next; + } upscli_authconf_t; + + upscli_authconf_t *upscli_create_authconf_item(const char *section); + + upscli_authconf_t *upscli_clone_authconf_item(upscli_authconf_t *source, const char *section); + + upscli_authconf_t *upscli_merge_authconf_item(upscli_authconf_t *source, upscli_authconf_t *target); + + upscli_authconf_t *upscli_free_authconf_item(upscli_authconf_t *node); +------ + +DESCRIPTION +----------- + +The *upscli_create_authconf_item()* function allocates the memory for an +`upscli_authconf_t` node and initializes it with the given section name. +Other string pointers remain `NULL`, numeric flags are set to `-1`, and +the `next` pointer is set to `NULL`. This item is not automatically added +to the list. + +The *upscli_clone_authconf_item()* function allocates the memory for an +`upscli_authconf_t` node and initializes it with the given section name +(if not NULL) and clones the values from the source node to the new node +by re-allocating non-NULL strings with `xstrdup()` and copying the numeric +flags. If the 'section' argument is provided and contains a non-trivial +`user@` component, the 'user' field if the structure is (re-)set to that +value. The `next` pointer is set to `NULL`. + +The *upscli_merge_authconf_item()* function copies the values from the +'source' node to the 'target' node, as long as the target node does not +already have a value for the same field (non-NULL possibly empty string, +or a non-negative numeric flag). The `next` pointer is not modified. +Like above, if the resulting section name in 'target' contains the `@` +character and some before it, the 'user' name field would be (re-)set +to that value and not cloned. + +Typically, the 'source' node when merging is the global configuration item +(or a `host:port` when merging for a section with specific user value), +and the 'target' node is a specific `host:port` or `user@host:port` +configuration item. + +The *upscli_free_authconf_item()* function frees the memory allocated for +a single `upscli_authconf_t` node and returns its `next` pointer. This is +useful for manually iterating and cleaning up copies of the list, although +typically linkman:upscli_free_authconf_list[3] is used to clear the entire +internal list. + +RETURN VALUE +------------ + +The *upscli_create_authconf_item()* and *upscli_clone_authconf_item()* +functions returns a pointer to the new structure, or `NULL` if an error +occurred. + +The *upscli_merge_authconf_item()* function returns a pointer to the merged +'target' structure. + +The *upscli_free_authconf_item()* function returns a pointer to the next element +of the `upscli_authconf_t` list, if known (and if it was part of the list). + +SEE ALSO +-------- + +linkman:upscli_dump_authconf_item[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_get_authconf_list[3], linkman:upscli_dump_authconf_list[3] diff --git a/docs/man/upscli_dump_authconf_item.txt b/docs/man/upscli_dump_authconf_item.txt new file mode 100644 index 0000000000..77f452582f --- /dev/null +++ b/docs/man/upscli_dump_authconf_item.txt @@ -0,0 +1,60 @@ +UPSCLI_DUMP_AUTHCONF_ITEM(3) +============================ + +NAME +---- + +upscli_dump_authconf_item, upscli_dump_authconf_list - Print authentication configuration node or list + +SYNOPSIS +-------- + +------ + #include + + int upscli_dump_authconf_item(FILE *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_item()* function prints a single configuration 'node' +to the specified 'stream'. If 'stream' is NULL, it defaults to `stdout`. + +The *upscli_dump_authconf_list()* function prints the entire internal list of +authentication configurations to the specified 'stream'. + +These functions are primarily intended for debugging purposes to verify the +content of the parsed configuration. + +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 +------------ + +The *upscli_dump_authconf_item()* function returns the return value of the +underlying linkman:fprintf[3] call, or -1 if 'node' is NULL. + +The *upscli_dump_authconf_list()* function returns the number of nodes +seen in the list (and likely printed). + +SEE ALSO +-------- + +linkman:upscli_create_authconf_item[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_get_authconf_list[3] diff --git a/docs/man/upscli_find_authconf_item.txt b/docs/man/upscli_find_authconf_item.txt new file mode 100644 index 0000000000..3d138b2813 --- /dev/null +++ b/docs/man/upscli_find_authconf_item.txt @@ -0,0 +1,122 @@ +UPSCLI_FIND_AUTHCONF_ITEM(3) +============================ + +NAME +---- + +upscli_find_authconf_item, upscli_get_authconf_item, +upscli_normalize_authconf_section_parts, +upscli_split_authconf_section - Find authentication configuration +items by their name components; help parse and normalize +name components for authentication configuration items + +SYNOPSIS +-------- + +------ + #include + + /* Item as it is on list, may be incomplete */ + upscli_authconf_t *upscli_find_authconf_item( + const char *user, + const char *host, + const char *port); + + /* Merge exact/host/global layers for a unique item */ + upscli_authconf_t *upscli_get_authconf_item( + const char *user, + const char *host, + const char *port, + int add_to_list); + + int upscli_normalize_authconf_section_parts( + char **out_normalized_sect_name, + char **p_sect_user, + int *out_fixed_sect_user, + char **p_sect_host, + char **p_sect_port); + + int upscli_split_authconf_section(const char *sect_name, + char **normalized_sect_name, + char **normalized_sect_user, + int *out_fixed_sect_user, + char **normalized_sect_host, + char **normalized_sect_port); +------ + +DESCRIPTION +----------- + +The *upscli_find_authconf_item()* function searches the internal list of +authentication configurations for the best match for the given 'user', +'host', and 'port', and returns that list entry "as is" (the way it was +originally spelled in configuration files, adjusted just for section +title normalization). This function is primarily a stepping stone for +*upscli_get_authconf_item()* to do its work. + +The matching logic follows this priority: + +1. Exact match for `[user@host:port]` +2. Match for `[@host:port]` (host default) +3. Global default section (if 'user', 'host', and 'port' are all NULL, + or if there was no exact or host default match) + +The *upscli_get_authconf_item()* function goes a bit further and finds "parent" +entries (from exact match, to host defaults, to global defaults) to merge any +missing fields in that section to be inherited from the higher-level defaults. +If a specific `user` value was requested and only a non-exact match was found, +that fixed `USER=...` directive will be assumed and injected into the output. + +If `add_to_list` is '0', this function would return a new instance of the +`upscli_authconf_t` structure, which owns a separate copy of any strings +involved, and can be safely discarded with linkman:upscli_free_authconf_item[3] +(and MUST be discarded by caller, it is not added into the list). Each call +with same arguments returns a new instance, even with same data (or different, +if e.g. global defaults were changed for fields not populated in the original +item on the list). + +If `add_to_list` is '1', this function would return a new instance of the +structure and add it into the list, or would edit an existing item already +on the list by filling in the missing points inherited from higher levels. +On one hand, the caller does not have to manage freeing of this structure +and repetitive calls do not increase memory usage; on another, this precludes +adaptation to changing higher-level defaults (which is a reasonable approach +when configuration is loaded once). + +The "*upscli_split_authconf_section()*" function splits a `sect_name` which may +be from a user-typed configuration file into user, host and port sections, and +with "*upscli_normalize_authconf_section_parts()*" normalizes the values (e.g. a +`NULL` 'host' becomes `localhost`, a missing 'port' is defaulted to `NUT_PORT` +defined at build configuration time, e.g. '3493' by default, and a non-numeric +string 'port' is resolved in the naming database of the current operating +environment). The resulting normalized values are returned to caller using +pointers provided in the arguments (if not `NULL` in case of +"*upscli_split_authconf_section()*", must be not `NULL` in case of +"*upscli_normalize_authconf_section_parts()*"). + +A 'normalized_sect_name' can be also constructed and returned, so that varying +but ultimately identical definitions of the section titles (e.g. `[@localhost]` +and `[@localhost:3493]` can be conflated when parsing configuration files or +searching in the list. + +RETURN VALUE +------------ + +The *upscli_find_authconf_item()* function returns a pointer to a `upscli_authconf_t` +structure containing the matched configuration, or NULL if no match is found. +Note that the returned pointer refers to an item in the internal list managed +by *libupsclient*; it should not be freed directly by the caller unless they +are managing their own list. + +The *upscli_free_authconf_item()* function returns the last known value of the `next` +pointer field from the node being freed. + +SEE ALSO +-------- + +linkman:upscli_read_authconf_file[3], +linkman:upscli_get_authconf_list[3], +linkman:upscli_free_authconf_list[3], +linkman:upscli_clone_authconf_item[3], +linkman:upscli_merge_authconf_item[3], +linkman:upscli_free_authconf_item[3] diff --git a/docs/man/upscli_free_authconf_list.txt b/docs/man/upscli_free_authconf_list.txt new file mode 100644 index 0000000000..67525afa79 --- /dev/null +++ b/docs/man/upscli_free_authconf_list.txt @@ -0,0 +1,34 @@ +UPSCLI_FREE_AUTHCONF_LIST(3) +============================ + +NAME +---- + +upscli_free_authconf_list - Free the list of authentication configurations + +SYNOPSIS +-------- + +------ + #include + + void upscli_free_authconf_list(void); +------ + +DESCRIPTION +----------- + +The *upscli_free_authconf_list()* function frees the memory allocated for the +entire list and resets the internal list pointer to NULL. + +RETURN VALUE +------------ + +The *upscli_free_authconf_list()* function returns nothing. + +SEE ALSO +-------- + +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_get_authconf_list[3], linkman:upscli_dump_authconf_list[3], +linkman:upscli_create_authconf_item[3], linkman:upscli_free_authconf_item[3] diff --git a/docs/man/upscli_get_authconf_list.txt b/docs/man/upscli_get_authconf_list.txt new file mode 100644 index 0000000000..518fd0fcbb --- /dev/null +++ b/docs/man/upscli_get_authconf_list.txt @@ -0,0 +1,40 @@ +UPSCLI_GET_AUTHCONF_LIST(3) +=========================== + +NAME +---- + +upscli_get_authconf_list - Get the list of known authentication configurations + +SYNOPSIS +-------- + +------ + #include + + upscli_authconf_t *upscli_get_authconf_list(void); +------ + +DESCRIPTION +----------- + +The *upscli_get_authconf_list()* function returns a pointer to the internal list +of authentication configurations parsed from the configuration file (usually +*nutauth.conf*) via linkman:upscli_read_authconf_file[3]. + +Each element in the list is of type `upscli_authconf_t` as detailed in +linkman:upscli_create_authconf_item[3]. + +RETURN VALUE +------------ + +The *upscli_get_authconf_list()* function returns a pointer to the first element +of the `upscli_authconf_t` list, or NULL if the list is empty or hasn't been +initialized by linkman:upscli_read_authconf_file[3]. + +SEE ALSO +-------- + +linkman:upscli_create_authconf_item[3], +linkman:upscli_read_authconf_file[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_free_authconf_list[3], linkman:upscli_dump_authconf_list[3] diff --git a/docs/man/upscli_init.txt b/docs/man/upscli_init.txt index 83aed550fa..5dff6f0573 100644 --- a/docs/man/upscli_init.txt +++ b/docs/man/upscli_init.txt @@ -4,7 +4,8 @@ UPSCLI_INIT(3) NAME ---- -upscli_init, upscli_init2 - Initialize upsclient module specifying security properties. +upscli_init, upscli_init2, upscli_init_authconf - Initialize upsclient module +specifying security properties. SYNOPSIS -------- @@ -26,6 +27,8 @@ SYNOPSIS const char *certname, const char *certpasswd, const char *certfile); + + int upscli_init_authconf(upscli_authconf_t *ac); ------ DESCRIPTION @@ -86,6 +89,10 @@ In both cases, the 'certname' (if not empty) can be used to verify that the specified file provides a certificate with expected subject name, or possibly matches the expected host name or IP address. +The *upscli_init_authconf()* function uses the `upscli_authconf_t` structure +populated by linkman:upscli_read_authconf_file[3] to pass equivalent information +from linkman:nutauth.conf[5] file(s). + Other nuances ------------- diff --git a/docs/man/upscli_read_authconf_file.txt b/docs/man/upscli_read_authconf_file.txt new file mode 100644 index 0000000000..08d5f7a54e --- /dev/null +++ b/docs/man/upscli_read_authconf_file.txt @@ -0,0 +1,64 @@ +UPSCLI_READ_AUTHCONF_FILE(3) +============================ + +NAME +---- + +upscli_read_authconf_file - Read the authentication configuration file + +SYNOPSIS +-------- + +------ + #include + + int upscli_read_authconf_file(const char *filename, int fatal_errors); +------ + +DESCRIPTION +----------- + +The *upscli_read_authconf_file()* function reads the specified 'filename' (which +is usually the path to *nutauth.conf*) and populates an internal list of +authentication and SSL configurations. + +If 'filename' is `NULL`, the function 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. + +If 'fatal_errors' is non-zero, the function may call abort the program on +critical failures (like memory allocation errors or if the file cannot +be opened). + +See the linkman:nutauth.conf[5] manual page for supported configuration +keywords. + +NESTING (INCLUDE FILES) +~~~~~~~~~~~~~~~~~~~~~~~ + +Included files are supported via the `INCLUDE` directive for optionally +present files, and `INCLUDE_REQUIRED` for files that must be there +(otherwise the program exits with a fatal error). + +Global-scope includes may modify global default items and define new sections. + +Section-scope includes (nested within a section) can only modify data +within that section. + +RETURN VALUE +------------ + +The *upscli_read_authconf_file()* function returns '1' on success, or '-1' if an +error occurs (and 'fatal_errors' was zero). + +SEE ALSO +-------- + +linkman:upscli_get_authconf_list[3], linkman:upscli_find_authconf_item[3], +linkman:upscli_dump_authconf_list[3], linkman:nutauth.conf[5] diff --git a/docs/nut.dict b/docs/nut.dict index c95c5a555f..3d46570d21 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3749 utf-8 +personal_ws-1.1 en 3754 utf-8 AAC AAS ABI @@ -1240,6 +1240,7 @@ SPS SRC SRVS SSD +SSLBACKEND SSLContext SSSS STARTTLS @@ -1690,6 +1691,7 @@ authPassword authPriv authProtocol authType +authconf autoboot autoconf autodetect @@ -2195,6 +2197,7 @@ formatconfig formatstring fosshost fp +fprintf freebsd freedesktop freeipmi @@ -2836,6 +2839,7 @@ numa numbatteries numlogins numq +nutauth nutclient nutclientmem nutconf @@ -3283,6 +3287,7 @@ spectype spellcheck spellchecked splitaddr +splitauth splitname sprintf squasher diff --git a/include/common.h b/include/common.h index d493ac7565..e4021e0164 100644 --- a/include/common.h +++ b/include/common.h @@ -423,6 +423,9 @@ int str_add_unique_token( /* Report maximum platform value for the pid_t */ pid_t get_max_pid_t(void); +/* Check filesystem permissions for files/dirs we deem secretive */ +void check_perms(const char *fn); + /* send sig to pid after some sanity checks, returns * -1 for error, or zero for a successfully sent signal */ int sendsignalpid(pid_t pid, int sig, const char *progname, int check_current_progname); diff --git a/scripts/obs/debian.nut-client.install b/scripts/obs/debian.nut-client.install index b899f84d60..b5356e1063 100644 --- a/scripts/obs/debian.nut-client.install +++ b/scripts/obs/debian.nut-client.install @@ -6,6 +6,7 @@ debian/tmp/sbin/upsmon debian/tmp/sbin/upssched debian/tmp/bin/upssched-cmd debian/tmp/etc/nut/nut.conf +debian/tmp/etc/nut/nutauth.conf debian/tmp/etc/nut/upsmon.conf debian/tmp/etc/nut/upssched.conf debian/tmp/usr/share/augeas/lenses/dist/nuthostsconf.aug diff --git a/scripts/obs/debian.nut-client.manpages b/scripts/obs/debian.nut-client.manpages index 115e821706..94744f65ce 100644 --- a/scripts/obs/debian.nut-client.manpages +++ b/scripts/obs/debian.nut-client.manpages @@ -4,6 +4,7 @@ debian/tmp/usr/share/man/man8/upsmon.8 debian/tmp/usr/share/man/man8/upsrw.8 debian/tmp/usr/share/man/man8/upssched.8 debian/tmp/usr/share/man/man5/nut.conf.5 +debian/tmp/usr/share/man/man5/nutauth.conf.5 debian/tmp/usr/share/man/man5/upsmon.conf.5 debian/tmp/usr/share/man/man5/upssched.conf.5 debian/tmp/usr/share/man/man8/upslog.8 diff --git a/scripts/obs/nut.spec b/scripts/obs/nut.spec index 158aca99c7..72fbb3c0d4 100644 --- a/scripts/obs/nut.spec +++ b/scripts/obs/nut.spec @@ -573,8 +573,8 @@ bin/chown -R %{NUT_USER} %{STATEPATH} || echo "WARNING: Could not secure state p bin/chgrp -R %{NUT_GROUP} %{STATEPATH} || echo "WARNING: Could not secure state path '%{STATEPATH}'" >&2 # Be sure that all files are owned by a dedicated user. bin/chown %{NUT_USER} %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 -bin/chgrp root %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 -bin/chmod 600 %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 +bin/chgrp root %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users %{CONFPATH}/nutauth.conf || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 +bin/chmod 600 %{CONFPATH}/upsd.conf %{CONFPATH}/upsmon.conf %{CONFPATH}/upsd.users %{CONFPATH}/nutauth.conf || echo "WARNING: Could not secure config files in path '%{CONFPATH}'" >&2 # And finally trigger udev to set permissions according to newly installed rules files. if [ -x /sbin/udevadm ] ; then /sbin/udevadm trigger --subsystem-match=usb --property-match=DEVTYPE=usb_device ; fi %if "x%{?systemdtmpfilesdir}" == "x" @@ -691,6 +691,7 @@ if [ -x /sbin/udevadm ] ; then /sbin/udevadm trigger --subsystem-match=usb --pro ### FIXME: if under /etc ### % config(noreplace) % {UDEVRULEPATH}/rules.d/*.rules %{UDEVRULEPATH}/rules.d/*.rules %config(noreplace) %{CONFPATH}/hosts.conf +%config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/nutauth.conf %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsd.conf %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsd.users %config(noreplace) %attr(600,%{NUT_USER},root) %{CONFPATH}/upsmon.conf diff --git a/server/upsd.c b/server/upsd.c index df887278b8..159ab233f1 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -2397,28 +2397,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 */ -} - int main(int argc, char **argv) { int opt_ret = 0, cmdret = 0, foreground = -1; diff --git a/server/upsd.h b/server/upsd.h index bb61fe373e..a2b2df128a 100644 --- a/server/upsd.h +++ b/server/upsd.h @@ -74,8 +74,6 @@ int send_err_extra(nut_ctype_t *client, const char *errtype, const char *extra); void server_load(void); void server_free(void); -void check_perms(const char *fn); - /* return values for instcmd / setvar status tracking, * mapped on drivers/upshandler.h, apart from STAT_PENDING (initial state) */ enum { diff --git a/server/user.h b/server/user.h index 613a0c5eef..c20eefef16 100644 --- a/server/user.h +++ b/server/user.h @@ -38,9 +38,6 @@ int user_checkaction(const char *un, const char *pw, const char *action); void user_flush(void); -/* cheat - we don't want the full upsd.h included here */ -void check_perms(const char *fn); - #ifdef __cplusplus /* *INDENT-OFF* */ } diff --git a/tests/Makefile.am b/tests/Makefile.am index 2bdab40017..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..6d82655579 --- /dev/null +++ b/tests/test_authconf.c @@ -0,0 +1,329 @@ +/* test_authconf.c - test program for client/authconf.c + * + * Copyright (C) 2026 Jim Klimov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" + +#include "common.h" +#include "authconf.h" + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + const char *test_conf = "test_nutauth.conf"; + const char *include_conf = "test_include.conf"; + FILE *f; + upscli_authconf_t *ac, *ac5, *ac7, *ac8, *ac9, *ac12; + size_t num_sections; + char buf[512], *s; + int l, testnum = 0; + + s = getenv("NUT_DEBUG_LEVEL"); + if (s && str_to_int(s, &l, 10) && l > 0) { + nut_debug_level = l; + upsdebugx(1, "Defaulting debug verbosity to NUT_DEBUG_LEVEL=%d " + "since none was requested by command-line options", l); + } + + if (argc > 1) { + upsdebugx(1, "Args ignored: '%s' etc.", argv[0]); + } + + /* Create dummy config files */ + f = fopen(test_conf, "w"); + if (!f) { + perror("fopen test_nutauth.conf"); + return 1; + } + fprintf(f, "USER = globaluser\n"); + fprintf(f, "PASS = globalpass\n"); + fprintf(f, "CERTVERIFY = 1\n"); + fprintf(f, "INCLUDE %s\n", include_conf); + fprintf(f, "[@localhost:12345]\n"); + fprintf(f, " USER = hostuser\n"); + fprintf(f, " FORCESSL = 1\n"); + fprintf(f, "[admin@localhost:12345]\n"); + fprintf(f, " PASS = adminpass\n"); + fprintf(f, " FORCESSL = 1\n"); + fclose(f); + + f = fopen(include_conf, "w"); + if (!f) { + perror("fopen test_include.conf"); + return 1; + } + fprintf(f, "[@otherhost]\n"); + fprintf(f, " USER = otheruser\n"); + fprintf(f, " CERTHOST = \"Other Server\"\n"); + fclose(f); + +#ifdef DEBUG + if (upscli_read_authconf_file(NULL, 0) != 1) { + fprintf(stderr, "INFO: Default read_authconf failed (no user/site-provided config found)\n"); + } +#endif + + /* 1. Expected file read */ + if (upscli_read_authconf_file(test_conf, 1) != 1) { + fprintf(stderr, "not ok %d - read_authconf failed\n", ++testnum); + return 1; + } + printf("ok %d - read_authconf did not fail\n", ++testnum); + + /* 2. Expected printout 1 */ + printf("=== Parsed configuration (production view):\n"); + /* Not "for_debug", but how would this info look in a config file */ + num_sections = upscli_dump_authconf_list(NULL, 0); + 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"); + + /* 4. Global match (no specific section for this host) */ + printf("Checking global match for '@somehost:port', and adding it to the list...\n"); + ac = upscli_get_authconf_item(NULL, "somehost", "port", 1); + if (ac) { + printf("Global match got user=%s\n", ac->user ? ac->user : "NULL"); + if (ac->user && strcmp(ac->user, "globaluser") == 0) { + printf("ok %d - Global match OK\n", ++testnum); + } else { + printf("not ok %d - Global match FAILED (wrong user)\n", ++testnum); + return 1; + } + } else { + printf("not ok %d - Global match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 5. Host default match, not saved */ + printf("Checking host default match for '@localhost:12345', not saved into list\n"); + ac5 = upscli_get_authconf_item(NULL, "localhost", "12345", 0); + if (ac5 && strcmp(ac5->user, "hostuser") == 0 && ac5->forcessl == 1 && ac5->certverify == 1) { + printf("ok %d - Host default match OK\n", ++testnum); + } else { + printf("not ok %d - Host default match FAILED\n", ++testnum); + if (ac5) + upscli_free_authconf_item(ac5); + return 1; + } + + /* 6. Exact match */ + printf("Checking exact match for 'admin@localhost:12345'\n"); + ac = upscli_get_authconf_item("admin", "localhost", "12345", 1); + if (ac) { + printf("Exact match: got user=%s pass=%s forcessl=%d\n", + ac->user ? ac->user : "NULL", + ac->pass ? ac->pass : "NULL", + ac->forcessl); + + if (ac->user && strcmp(ac->user, "admin") == 0 + && ac->pass && strcmp(ac->pass, "adminpass") == 0 + && ac->forcessl == 1 + ) { + printf("ok %d - Exact match OK\n", ++testnum); + } else { + printf("not ok %d - Exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "admin", "adminpass"); + return 1; + } + } else { + printf("not ok %d - Exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 7. Non-exact match */ + printf("Checking non-exact match for 'somebody@localhost:12345'\n"); + ac7 = upscli_get_authconf_item("somebody", "localhost", "12345", 0); + if (ac7) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac7->user ? ac7->user : "NULL", + ac7->pass ? ac7->pass : "NULL", + ac7->forcessl); + + if (ac7->user && strcmp(ac7->user, "somebody") == 0 + && ac7->pass && strcmp(ac7->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac7->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 8. Host default match, saved to list */ + printf("Checking host default match for '@localhost:12345' and saving into list\n"); + ac8 = upscli_get_authconf_item(NULL, "localhost", "12345", 1); + if (ac8 && strcmp(ac8->user, "hostuser") == 0 && ac8->forcessl == 1 && ac8->certverify == 1) { + printf("ok %d - Host default match OK\n", ++testnum); + } else { + printf("not ok %d - Host default match FAILED\n", ++testnum); + return 1; + } + + /* 9. Non-exact match, take 2 */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, and adding it to the list\n"); + ac9 = upscli_get_authconf_item("somebody", "localhost", "12345", 1); + if (ac9) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac9->user ? ac9->user : "NULL", + ac9->pass ? ac9->pass : "NULL", + ac9->forcessl); + + if (ac9->user && strcmp(ac9->user, "somebody") == 0 + && ac9->pass && strcmp(ac9->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac9->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + + /* 10. Same non-exact match */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, should be same pointer\n"); + ac = upscli_get_authconf_item("somebody", "localhost", "12345", 1); + if (ac) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac->user ? ac->user : "NULL", + ac->pass ? ac->pass : "NULL", + ac->forcessl); + + if (ac->user && strcmp(ac->user, "somebody") == 0 + && ac->pass && strcmp(ac->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac)\n", ++testnum); + return 1; + } + /* 11. Same non-exact match - continued */ + if (ac == ac9) { + printf("ok %d - Non-exact match OK and returned same pointer to item in the list\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (did not return same pointer to item in the list)\n", ++testnum); + return 1; + } + + /* 12. Same non-exact match but not in the list */ + printf("Checking non-exact match for 'somebody@localhost:12345' after list modification, but not adding to list, should be a different pointer\n"); + ac12 = upscli_get_authconf_item("somebody", "localhost", "12345", 0); + if (ac12) { + printf("Non-exact match: got user=%s pass=%s forcessl=%d\n", + ac12->user ? ac12->user : "NULL", + ac12->pass ? ac12->pass : "NULL", + ac12->forcessl); + + if (ac12->user && strcmp(ac12->user, "somebody") == 0 + && ac12->pass && strcmp(ac12->pass, "globalpass") == 0 /* replaced from NULL originally in @localhost:12345 */ + && ac12->forcessl == 1 + ) { + printf("ok %d - Non-exact match OK\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (wrong values): expecting user='%s' pass='%s'\n", + ++testnum, "somebody", "globalpass"); + return 1; + } + } else { + printf("not ok %d - Non-exact match FAILED (no ac12)\n", ++testnum); + return 1; + } + /* 13. Same non-exact match - continued */ + if (ac12 != ac9) { + printf("ok %d - Non-exact match OK and did not return same pointer to item in the list\n", ++testnum); + } else { + printf("not ok %d - Non-exact match FAILED (returned same pointer to item in the list but should have been a clone)\n", ++testnum); + return 1; + } + + /* 14. Include match */ + printf("Checking include match for '@otherhost'\n"); + ac = upscli_get_authconf_item(NULL, "otherhost", NULL, 1); + snprintf(buf, sizeof(buf), "@otherhost:%u", (unsigned int)NUT_PORT); + if (ac + && ac->section && strcmp(ac->section, buf) == 0 + && ac->user && strcmp(ac->user, "otheruser") == 0 + && ac->certhost && strcmp(ac->certhost, "Other Server") == 0 + ) { + printf("ok %d - Include match OK\n", ++testnum); + } else { + if (ac) { + printf("not ok %d - Include match FAILED: got section=%s user=%s\n", + ++testnum, + ac->section ? ac->section : "NULL", + ac->user ? ac->user : "NULL"); + } else { + printf("not ok %d - Include match FAILED: no ac\n", ++testnum); + } + return 1; + } + + /* 15. No bogus hits */ + printf("Checking NO match for '@otherhost:portnum' other than global section\n"); + ac = upscli_find_authconf_item(NULL, "otherhost", "portnum"); + if (ac) { + if (!(ac->section) || !*(ac->section)) { + printf("ok %d - No bogus match OK: got global section\n", ++testnum); + } else { + printf("not ok %d - No bogus match FAILED: had a hit\n", ++testnum); + upscli_dump_authconf_item(NULL, ac, 1); + return 1; + } + } else { + printf("ok %d - No bogus match kind of OK: got no ac\n", ++testnum); + } + + /* 16. Expected printout 3 */ + printf("=== Parsed configuration (production view) after several 'get' operations with results caching:\n"); + /* Not "for_debug", but how would this info look in a config file */ + num_sections = upscli_dump_authconf_list(NULL, 0); + printf("===== Collected %" PRIuSIZE " sections\n\n", num_sections); + /* Added '@somehost:port' and 'somebody@...' */ + printf("%sok %d - parsed 6 sections\n", num_sections == 6 ? "" : "not ", ++testnum); + + upscli_free_authconf_item(ac5); + upscli_free_authconf_item(ac7); + upscli_free_authconf_item(ac12); + /* do not free ac8 and ac9 - they are added to list */ + + upscli_free_authconf_list(); + unlink(test_conf); + unlink(include_conf); + + printf("All tests passed!\n"); + return 0; +}