From b2e08b8355c76a2adb03e71025137a628db71149 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 6 Feb 2026 16:00:20 +0100 Subject: [PATCH 01/11] clients/upsimage.c, clients/upsset.c, clients/upsstats.c: fix indentation for recent fixes and update (C) heading [#3219] Signed-off-by: Jim Klimov --- clients/upsimage.c | 17 +++++++++-------- clients/upsset.c | 17 +++++++++-------- clients/upsstats.c | 17 +++++++++-------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/clients/upsimage.c b/clients/upsimage.c index 1d99fdac15..a055cab28a 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -20,6 +20,7 @@ Copyrights: (C) 1998 Russell Kroll (C) 2002 Simon Rozman + (C) 2020-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 @@ -619,14 +620,14 @@ int main(int argc, char **argv) double var = 0; #ifdef WIN32 - /* Required ritual before calling any socket functions */ - static WSADATA WSAdata; - static int WSA_Started = 0; - if (!WSA_Started) { - WSAStartup(2, &WSAdata); - atexit((void(*)(void))WSACleanup); - WSA_Started = 1; - } + /* Required ritual before calling any socket functions */ + static WSADATA WSAdata; + static int WSA_Started = 0; + if (!WSA_Started) { + WSAStartup(2, &WSAdata); + atexit((void(*)(void))WSACleanup); + WSA_Started = 1; + } /* Avoid binary output conversions, e.g. * mangling what looks like CRLF on WIN32 */ diff --git a/clients/upsset.c b/clients/upsset.c index 898acd4dba..83abe0710e 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1,6 +1,7 @@ /* upsset - CGI program to manage read/write variables Copyright (C) 1999 Russell Kroll + Copyright (C) 2020-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 @@ -1116,14 +1117,14 @@ int main(int argc, char **argv) int i; #ifdef WIN32 - /* Required ritual before calling any socket functions */ - static WSADATA WSAdata; - static int WSA_Started = 0; - if (!WSA_Started) { - WSAStartup(2, &WSAdata); - atexit((void(*)(void))WSACleanup); - WSA_Started = 1; - } + /* Required ritual before calling any socket functions */ + static WSADATA WSAdata; + static int WSA_Started = 0; + if (!WSA_Started) { + WSAStartup(2, &WSAdata); + atexit((void(*)(void))WSACleanup); + WSA_Started = 1; + } /* Avoid binary output conversions, e.g. * mangling what looks like CRLF on WIN32 */ diff --git a/clients/upsstats.c b/clients/upsstats.c index 5a0bf47521..189d7bc368 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -2,6 +2,7 @@ Copyright (C) 1998 Russell Kroll Copyright (C) 2005 Arnaud Quette + Copyright (C) 2020-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 @@ -1489,14 +1490,14 @@ int main(int argc, char **argv) int i; #ifdef WIN32 - /* Required ritual before calling any socket functions */ - static WSADATA WSAdata; - static int WSA_Started = 0; - if (!WSA_Started) { - WSAStartup(2, &WSAdata); - atexit((void(*)(void))WSACleanup); - WSA_Started = 1; - } + /* Required ritual before calling any socket functions */ + static WSADATA WSAdata; + static int WSA_Started = 0; + if (!WSA_Started) { + WSAStartup(2, &WSAdata); + atexit((void(*)(void))WSACleanup); + WSA_Started = 1; + } /* Avoid binary output conversions, e.g. * mangling what looks like CRLF on WIN32 */ From 77c206ba2be0f36572d10f7d289b16ac603843d9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 6 Feb 2026 16:05:37 +0100 Subject: [PATCH 02/11] clients/upsstats.c: parameterize template_single and template_list filenames with legacy defaults [#2524] Signed-off-by: Jim Klimov --- clients/upsstats.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/clients/upsstats.c b/clients/upsstats.c index 189d7bc368..b7610ba42e 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -63,7 +63,9 @@ static char *monhostdesc = NULL; static uint16_t port; static char *upsname, *hostname; -static char *upsimgpath="upsimage.cgi" EXEEXT, *upsstatpath="upsstats.cgi" EXEEXT; +static char *upsimgpath="upsimage.cgi" EXEEXT, *upsstatpath="upsstats.cgi" EXEEXT, + *template_single = "upsstats-single.html", + *template_list = "upsstats.html"; static UPSCONN_t ups; static FILE *tf; @@ -1327,7 +1329,7 @@ static void display_single(void) if (treemode) display_tree(1); else - display_template("upsstats-single.html"); + display_template(template_single); upscli_disconnect(&ups); upsdebug_call_finished0(); @@ -1559,7 +1561,7 @@ int main(int argc, char **argv) /* default: multimon replacement mode */ load_hosts_conf(); currups = ulhead; - display_template("upsstats.html"); + display_template(template_list); } /* Clean up memory */ From 7cca1ccd432cad7d1dec043fb547644e808684b4 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 6 Feb 2026 16:34:36 +0100 Subject: [PATCH 03/11] clients/upsstats.c, docs/man/upsstats.cgi.txt, NEWS.adoc: support providing CGI options for alternate template file names (co-located with original ones) [#2524, #3304] Signed-off-by: Jim Klimov --- NEWS.adoc | 2 + clients/upsstats.c | 79 +++++++++++++++++++++++++++++++++++---- docs/man/upsstats.cgi.txt | 5 +++ 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index b618b57d3c..2b1d469ff4 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -338,6 +338,8 @@ several `FSD` notifications into one executed action. [PR #3097] * Introduced a `@NUT_UPSSTATS_TEMPLATE@` command which the HTML template files now MUST start with (safety check that we are reading a template). [issue #3252, PR #3249] + * (Experimental) Custom templates other than `upsstats{,-single}.html` can + now be specified as CGI parameters. [issue #2524, PR #3304] - `upssched` tool updates: * Previously in PR #2896 (NUT releases v2.8.3 and v2.8.4) the `UPSNAME` and diff --git a/clients/upsstats.c b/clients/upsstats.c index b7610ba42e..fefec01ff5 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -63,9 +63,8 @@ static char *monhostdesc = NULL; static uint16_t port; static char *upsname, *hostname; -static char *upsimgpath="upsimage.cgi" EXEEXT, *upsstatpath="upsstats.cgi" EXEEXT, - *template_single = "upsstats-single.html", - *template_list = "upsstats.html"; +static char *upsimgpath = "upsimage.cgi" EXEEXT, *upsstatpath = "upsstats.cgi" EXEEXT, + *template_single = NULL, *template_list = NULL; static UPSCONN_t ups; static FILE *tf; @@ -102,6 +101,18 @@ void parsearg(char *var, char *value) output_json = 1; } + if (!strcmp(var, "template_single")) { + /* Error-checking in display_template(), when we have all options in place */ + free(template_single); + template_single = xstrdup(value); + } + + if (!strcmp(var, "template_list")) { + /* Error-checking in display_template(), when we have all options in place */ + free(template_list); + template_list = xstrdup(value); + } + upsdebug_call_finished0(); } @@ -529,6 +540,14 @@ static void do_hostlink(void) printf("sys); + if (strcmp(template_single, "upsstats-single.html")) { + printf("&template_single=%s", template_single); + } + + if (strcmp(template_list, "upsstats.html")) { + printf("&template_list=%s", template_list); + } + if (refreshdelay > 0) { printf("&refresh=%d", refreshdelay); } @@ -546,8 +565,22 @@ static void do_treelink_json(const char *text) return; } - printf("%s", - upsstatpath, currups->sys, + printf("sys); + + if (strcmp(template_single, "upsstats-single.html")) { + printf("&template_single=%s", template_single); + } + + if (strcmp(template_list, "upsstats.html")) { + printf("&template_list=%s", template_list); + } + + if (refreshdelay > 0) { + printf("&refresh=%d", refreshdelay); + } + + printf("\">%s", ((text && *text) ? text : "JSON")); upsdebug_call_finished0(); @@ -562,8 +595,22 @@ static void do_treelink(const char *text) return; } - printf("%s", - upsstatpath, currups->sys, + printf("sys); + + if (strcmp(template_single, "upsstats-single.html")) { + printf("&template_single=%s", template_single); + } + + if (strcmp(template_list, "upsstats.html")) { + printf("&template_list=%s", template_list); + } + + if (refreshdelay > 0) { + printf("&refresh=%d", refreshdelay); + } + + printf("\">%s", ((text && *text) ? text : "All data")); upsdebug_call_finished0(); @@ -1096,6 +1143,16 @@ static void display_template(const char *tfn) upsdebug_call_starting_for_str1(tfn); + if (!tfn || !*tfn || strstr(tfn, "/") || strstr(tfn, "\\") || !strstr(tfn, ".htm")) { + /* We only allow pre-configured templates in one managed location, with ".htm" in the name */ + fprintf(stderr, "upsstats: Can't open %s: %s: asked to look not exactly in the managed location\n", fn, strerror(errno)); + + printf("Error: can't open template file (%s): asked to look not exactly in the managed location\n", tfn); + + upsdebug_call_finished1(": subdir in template"); + exit(EXIT_FAILURE); + } + snprintf(fn, sizeof(fn), "%s/%s", confpath(), tfn); tf = fopen(fn, "rb"); @@ -1519,6 +1576,10 @@ int main(int argc, char **argv) nut_debug_level = i; } + /* Built-in defaults */ + template_single = xstrdup("upsstats-single.html"); + template_list = xstrdup("upsstats.html"); + extractcgiargs(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); @@ -1544,6 +1605,8 @@ int main(int argc, char **argv) } free(upsname); free(hostname); + free(template_single); + free(template_list); exit(EXIT_SUCCESS); } @@ -1576,6 +1639,8 @@ int main(int argc, char **argv) free(ulhead); ulhead = currups; } + free(template_single); + free(template_list); return 0; } diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index b83fca8c13..5665e28560 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -48,9 +48,14 @@ The web page that is displayed is actually a template containing commands to `upsstats` which are replaced by status information. The default file used for the overview of devices is `upsstats.html`. +An alternate template may be provided by `&template_list=...` CGI query option. When monitoring a single UPS, the file displayed is `upsstats-single.html`. +An alternate template may be provided by `&template_single=...` CGI query option. + +Alternate templates must be structured similarly to default ones, located in the +same directory, and contain `.htm` in the file name. The format of these files, including the possible commands, is documented in linkman:upsstats.html[5]. From 9f8740cc3eb6391e8cea148ea278612a951c241f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 10:27:31 +0100 Subject: [PATCH 04/11] clients/cgilib.c: debug-trace also extractcgiargs() with what query-string text we pass into caller parsearg() implementations [#2524, #3219] Signed-off-by: Jim Klimov --- clients/cgilib.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clients/cgilib.c b/clients/cgilib.c index 36bc0395b8..dba55b5ffb 100644 --- a/clients/cgilib.c +++ b/clients/cgilib.c @@ -111,6 +111,8 @@ void extractcgiargs(void) cleanvar = unescape(varname); cleanval = unescape(value); + upsdebugx(3, "%s: parsearg('%s', '%s')
", + __func__, NUT_STRARG(cleanvar), NUT_STRARG(cleanval)); parsearg(cleanvar, cleanval); free(cleanvar); free(cleanval); From e27269497069fbb2483d67056eb6a57c6914fcb8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 12:46:33 +0100 Subject: [PATCH 05/11] common/common.c, include/common.h: introduce UPSLOG_STDOUT bit and support in vupslog(), and cgilogbit_set() for browser-friendly logging in NUT CGI programs [#2524] Signed-off-by: Jim Klimov --- common/common.c | 51 +++++++++++++++++++++++++++++++++++++++++------- include/common.h | 11 +++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/common/common.c b/common/common.c index 50803b25f4..832871ae77 100644 --- a/common/common.c +++ b/common/common.c @@ -588,6 +588,16 @@ int syslog_is_disabled(void) return value; } +/* enable writing upslog_with_errno() and upslogx() type messages to + * the stdout instead of stderr, and end them with HTML
tag, + * to help troubleshoot NUT CGI programs specifically */ +void cgilogbit_set(void) +{ + xbit_set(&upslog_flags, UPSLOG_STDOUT); + xbit_set(&upslog_flags, UPSLOG_CGI_BR); + xbit_clear(&upslog_flags, UPSLOG_STDERR); +} + /* enable writing upslog_with_errno() and upslogx() type messages to the syslog */ void syslogbit_set(void) @@ -3927,7 +3937,7 @@ static void vupslog(int priority, const char *fmt, va_list va, int use_strerror) upslog_start = now; } - if (xbit_test(upslog_flags, UPSLOG_STDERR)) { + if (xbit_test(upslog_flags, UPSLOG_STDERR) || xbit_test(upslog_flags, UPSLOG_STDOUT)) { if (nut_debug_level > 0) { struct timeval now; @@ -3940,15 +3950,42 @@ static void vupslog(int priority, const char *fmt, va_list va, int use_strerror) /* Print all in one shot, to better avoid * mixed lines in parallel threads */ - fprintf(stderr, "%4.0f.%06ld\t%s\n", - difftime(now.tv_sec, upslog_start.tv_sec), - (long)(now.tv_usec - upslog_start.tv_usec), - buf); + if (xbit_test(upslog_flags, UPSLOG_STDERR)) { +#ifdef WIN32 + fflush(stderr); +#endif /* WIN32 */ + fprintf(stderr, "%s%4.0f.%06ld\t%s%s\n", + xbit_test(upslog_flags, UPSLOG_CGI_BR) ? "
" : "",
+					difftime(now.tv_sec, upslog_start.tv_sec),
+					(long)(now.tv_usec - upslog_start.tv_usec),
+					buf,
+					xbit_test(upslog_flags, UPSLOG_CGI_BR) ? "
" : "" + ); + } + + if (xbit_test(upslog_flags, UPSLOG_STDOUT)) { +#ifdef WIN32 + fflush(stdout); +#endif /* WIN32 */ + fprintf(stdout, "%s%4.0f.%06ld\t%s%s\n", + xbit_test(upslog_flags, UPSLOG_CGI_BR) ? "
" : "",
+					difftime(now.tv_sec, upslog_start.tv_sec),
+					(long)(now.tv_usec - upslog_start.tv_usec),
+					buf,
+					xbit_test(upslog_flags, UPSLOG_CGI_BR) ? "
" : "" + ); + } } else { - fprintf(stderr, "%s\n", buf); + if (xbit_test(upslog_flags, UPSLOG_STDERR)) + fprintf(stderr, "%s\n", buf); + if (xbit_test(upslog_flags, UPSLOG_STDOUT)) + fprintf(stdout, "%s\n", buf); } #ifdef WIN32 - fflush(stderr); + if (xbit_test(upslog_flags, UPSLOG_STDERR)) + fflush(stderr); + if (xbit_test(upslog_flags, UPSLOG_STDOUT)) + fflush(stdout); #endif /* WIN32 */ } if (xbit_test(upslog_flags, UPSLOG_SYSLOG)) diff --git a/include/common.h b/include/common.h index 39987916ca..85bb4e8291 100644 --- a/include/common.h +++ b/include/common.h @@ -446,6 +446,11 @@ int sendsignalfnaliases(const char *pidfn, const char * sig, const char **progna * caller should strdup() a copy to retain beyond the lifetime of "file" */ const char *xbasename(const char *file); +/* enable writing upslog_with_errno() and upslogx() type messages to + * the stdout instead of stderr, and end them with HTML
tag, + * to help troubleshoot NUT CGI programs specifically */ +void cgilogbit_set(void); + /* enable writing upslog_with_errno() and upslogx() type messages to the syslog */ void syslogbit_set(void); @@ -755,6 +760,12 @@ extern int optind; #define UPSLOG_STDERR_ON_FATAL 0x0004 #define UPSLOG_SYSLOG_ON_FATAL 0x0008 +/* Special cases, primarily for NUT CGI programs to dump logs + * in a way better usable when troubleshooting with a browser: + */ +#define UPSLOG_STDOUT 0x0010 +#define UPSLOG_CGI_BR 0x0020 + #ifndef HAVE_SETEUID # define seteuid(x) setresuid(-1,x,-1) /* Works for HP-UX 10.20 */ # define setegid(x) setresgid(-1,x,-1) /* Works for HP-UX 10.20 */ From 8818a3aba7fc4579ded9afa3ee56896309d54671 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 12:48:00 +0100 Subject: [PATCH 06/11] clients/ups{image,set,stats}.c: introduce support for browser-friendly logging in NUT CGI programs (in developer build and/or command-line runs) [#2524] Signed-off-by: Jim Klimov --- clients/upsimage.c | 17 +++++++++++++++++ clients/upsset.c | 14 ++++++++++++++ clients/upsstats.c | 17 +++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/clients/upsimage.c b/clients/upsimage.c index a055cab28a..18d7cf882b 100644 --- a/clients/upsimage.c +++ b/clients/upsimage.c @@ -647,6 +647,23 @@ int main(int argc, char **argv) nut_debug_level = i; } +#ifdef NUT_CGI_DEBUG_UPSIMAGE +# if (NUT_CGI_DEBUG_UPSIMAGE - 0 < 1) +# undef NUT_CGI_DEBUG_UPSIMAGE +# define NUT_CGI_DEBUG_UPSIMAGE 6 +# endif + /* Un-comment via make flags when developer-troubleshooting: */ + nut_debug_level = NUT_CGI_DEBUG_UPSIMAGE; +#endif + + if (nut_debug_level > 0) { + cgilogbit_set(); + printf("Content-type: text/html\n"); + printf("Pragma: no-cache\n"); + printf("\n"); + printf("

NUT CGI Debugging enabled, level: %d

\n\n", nut_debug_level); + } + extractcgiargs(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); diff --git a/clients/upsset.c b/clients/upsset.c index 83abe0710e..2ac3a5d052 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1149,6 +1149,20 @@ int main(int argc, char **argv) nut_debug_level = i; } +#ifdef NUT_CGI_DEBUG_UPSSET +# if (NUT_CGI_DEBUG_UPSSET - 0 < 1) +# undef NUT_CGI_DEBUG_UPSSET +# define NUT_CGI_DEBUG_UPSSET 6 +# endif + /* Un-comment via make flags when developer-troubleshooting: */ + nut_debug_level = NUT_CGI_DEBUG_UPSSET; +#endif + + if (nut_debug_level > 0) { + cgilogbit_set(); + printf("

NUT CGI Debugging enabled, level: %d

\n\n", nut_debug_level); + } + /* see if the magic string is present in the config file */ check_conf(); diff --git a/clients/upsstats.c b/clients/upsstats.c index fefec01ff5..a26f79256d 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -1580,6 +1580,23 @@ int main(int argc, char **argv) template_single = xstrdup("upsstats-single.html"); template_list = xstrdup("upsstats.html"); +#ifdef NUT_CGI_DEBUG_UPSSTATS +# if (NUT_CGI_DEBUG_UPSSTATS - 0 < 1) +# undef NUT_CGI_DEBUG_UPSSTATS +# define NUT_CGI_DEBUG_UPSSTATS 6 +# endif + /* Un-comment via make flags when developer-troubleshooting: */ + nut_debug_level = NUT_CGI_DEBUG_UPSSTATS; +#endif + + if (nut_debug_level > 0) { + cgilogbit_set(); + printf("Content-type: text/html\n"); + printf("Pragma: no-cache\n"); + printf("\n"); + printf("

NUT CGI Debugging enabled, level: %d

\n\n", nut_debug_level); + } + extractcgiargs(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); From 38a9de1e7048567c4f0b39ff65ac92d52a1caf98 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 12:48:48 +0100 Subject: [PATCH 07/11] clients/upsset.c: use "Pragma: no-cache" HTTP header as in upsstats.cgi [#2524] Signed-off-by: Jim Klimov --- clients/upsset.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clients/upsset.c b/clients/upsset.c index 2ac3a5d052..3074bf63ef 100644 --- a/clients/upsset.c +++ b/clients/upsset.c @@ -1137,7 +1137,9 @@ int main(int argc, char **argv) NUT_UNUSED_VARIABLE(argv); username = password = function = monups = NULL; - printf("Content-type: text/html\n\n"); + printf("Content-type: text/html\n"); + printf("Pragma: no-cache\n"); + printf("\n"); /* NOTE: Caller must `export NUT_DEBUG_LEVEL` to see debugs for upsc * and NUT methods called from it. This line aims to just initialize From 15308355ba8f7389478af1ff632951b44351bee5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 13:08:39 +0100 Subject: [PATCH 08/11] clients/cgilib.c: extractcgiargs(): fix processing of query strings with flag tokens (no assignment) in the middle, not at end [#2524] Signed-off-by: Jim Klimov --- clients/cgilib.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/clients/cgilib.c b/clients/cgilib.c index dba55b5ffb..044432c1b0 100644 --- a/clients/cgilib.c +++ b/clients/cgilib.c @@ -87,8 +87,15 @@ void extractcgiargs(void) while (ptr) { varname = ptr; eq = strchr(varname, '='); - if (!eq) { - ptr = strchr(varname, '&'); + amp = strchr(varname, '&'); + if (!eq + || (eq && amp && amp < eq) + ) { + /* Last token is a flag (without assignment in sight), + * OR we've got a flag token in the middle of a query + * string, followed by another key=value pair later on. + */ + ptr = amp; if (ptr) *ptr++ = '\0'; @@ -99,6 +106,8 @@ void extractcgiargs(void) continue; } + /* The nearest point of interest is a key=value pair, + * maybe followed by another amp and flag or assignment... */ *eq = '\0'; value = eq + 1; amp = strchr(value, '&'); From c21672df4f529be833d736f488a518540c2bcb63 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 14:06:54 +0100 Subject: [PATCH 09/11] clients/upsstats.c: do_*link*() methods: err on the safe side and NULL-check the template_list/single vars before comparing strings [#2524] Signed-off-by: Jim Klimov --- clients/upsstats.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clients/upsstats.c b/clients/upsstats.c index a26f79256d..923197a5f1 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -540,11 +540,11 @@ static void do_hostlink(void) printf("sys); - if (strcmp(template_single, "upsstats-single.html")) { + if (template_single && strcmp(template_single, "upsstats-single.html")) { printf("&template_single=%s", template_single); } - if (strcmp(template_list, "upsstats.html")) { + if (template_list && strcmp(template_list, "upsstats.html")) { printf("&template_list=%s", template_list); } @@ -568,11 +568,11 @@ static void do_treelink_json(const char *text) printf("sys); - if (strcmp(template_single, "upsstats-single.html")) { + if (template_single && strcmp(template_single, "upsstats-single.html")) { printf("&template_single=%s", template_single); } - if (strcmp(template_list, "upsstats.html")) { + if (template_list && strcmp(template_list, "upsstats.html")) { printf("&template_list=%s", template_list); } @@ -598,11 +598,11 @@ static void do_treelink(const char *text) printf("sys); - if (strcmp(template_single, "upsstats-single.html")) { + if (template_single && strcmp(template_single, "upsstats-single.html")) { printf("&template_single=%s", template_single); } - if (strcmp(template_list, "upsstats.html")) { + if (template_list && strcmp(template_list, "upsstats.html")) { printf("&template_list=%s", template_list); } From 1d2aee1a8ab2d16aa06cce1f08a75956c266ce67 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 14:31:36 +0100 Subject: [PATCH 10/11] Introduce CUSTOM_TEMPLATE_LIST and CUSTOM_TEMPLATE_SINGLE settings in NUT CGI hosts.conf to constrain the permitted custom HTML template file names to a set that admins intend to use [#2524] Signed-off-by: Jim Klimov --- NEWS.adoc | 3 +- clients/upsstats.c | 171 ++++++++++++++++++++++++++++++++++---- conf/hosts.conf.sample | 33 +++++++- docs/man/hosts.conf.txt | 20 +++++ docs/man/upsstats.cgi.txt | 20 +++-- 5 files changed, 223 insertions(+), 24 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 2b1d469ff4..ad32b08d8c 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -339,7 +339,8 @@ several `FSD` notifications into one executed action. [PR #3097] files now MUST start with (safety check that we are reading a template). [issue #3252, PR #3249] * (Experimental) Custom templates other than `upsstats{,-single}.html` can - now be specified as CGI parameters. [issue #2524, PR #3304] + now be specified as CGI parameters, if locally permitted via `hosts.conf`. + [issue #2524, PR #3304] - `upssched` tool updates: * Previously in PR #2896 (NUT releases v2.8.3 and v2.8.4) the `UPSNAME` and diff --git a/clients/upsstats.c b/clients/upsstats.c index 923197a5f1..0cfe4b3929 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -70,7 +70,11 @@ static UPSCONN_t ups; static FILE *tf; static long forofs = 0; -static ulist_t *ulhead = NULL, *currups = NULL; +static ulist_t *ulhead = NULL, *currups = NULL, + /* hijack the linked-list structure to store + * just filenames (as "sys") so far */ + *allowed_template_single_lhead = NULL, + *allowed_template_list_lhead = NULL; static int skip_clause = 0, skip_block = 0; @@ -1137,22 +1141,68 @@ static void parse_line(const char *buf) upsdebug_call_finished0(); } -static void display_template(const char *tfn) +/* type = 1 for upsstats-single.html (or custom copy), 2 for upsstats.html (list) */ +static void display_template(const char *tfn, int type) { char fn[NUT_PATH_MAX + 1], buf[LARGEBUF]; + ulist_t *tmp = NULL; upsdebug_call_starting_for_str1(tfn); - if (!tfn || !*tfn || strstr(tfn, "/") || strstr(tfn, "\\") || !strstr(tfn, ".htm")) { + if (!tfn || !*tfn || strstr(tfn, "/") || strstr(tfn, "\\")) { /* We only allow pre-configured templates in one managed location, with ".htm" in the name */ - fprintf(stderr, "upsstats: Can't open %s: %s: asked to look not exactly in the managed location\n", fn, strerror(errno)); + errno = EPERM; + fprintf(stderr, "upsstats: Can't open %s: %s: asked to look not exactly in the managed location\n", tfn, strerror(errno)); - printf("Error: can't open template file (%s): asked to look not exactly in the managed location\n", tfn); + printf("Error: can't open template file (%s): Not authorized\n", tfn); upsdebug_call_finished1(": subdir in template"); exit(EXIT_FAILURE); } + if (!strstr(tfn, ".htm")) { + /* We only allow pre-configured templates with ".htm" in the name */ + errno = EPERM; + fprintf(stderr, "upsstats: Can't open %s: %s: asked to look at not a *.htm* file\n", tfn, strerror(errno)); + + printf("Error: can't open template file (%s): Not authorized\n", tfn); + + upsdebug_call_finished1(": not a *.htm* file"); + exit(EXIT_FAILURE); + } + + if (type == 1) { + /* Check if [custom] single template is allowed + * (built-in/legacy default starts the list) */ + tmp = allowed_template_single_lhead; + while (tmp) { + if (!strcmp(tmp->sys, tfn)) + break; + tmp = (ulist_t *)tmp->next; + } + } else + if (type == 2) { + /* Check if [custom] list template is allowed + * (built-in/legacy default starts the list) */ + tmp = allowed_template_list_lhead; + while (tmp) { + if (!strcmp(tmp->sys, tfn)) + break; + tmp = (ulist_t *)tmp->next; + } + } + + if (!tmp) { + /* We only allow pre-configured templates permitted via hosts.conf */ + errno = EPERM; + fprintf(stderr, "upsstats: Can't open %s: %s: Not authorized: template not permitted via hosts.conf\n", tfn, strerror(errno)); + + printf("Error: can't open template file (%s): Not authorized\n", tfn); + + upsdebug_call_finished1(": template not permitted via hosts.conf"); + exit(EXIT_FAILURE); + } + snprintf(fn, sizeof(fn), "%s/%s", confpath(), tfn); tf = fopen(fn, "rb"); @@ -1291,13 +1341,69 @@ static void add_ups(char *sys, char *desc) ulhead = tmp; } +static void add_allowed_template_list(char *tfn) +{ + ulist_t *tmp, *last; + + if (!tfn || !*tfn) + return; + + tmp = last = allowed_template_list_lhead; + + while (tmp) { + if (!strcmp(tmp->sys, tfn)) + return; + last = tmp; + tmp = (ulist_t *)tmp->next; + } + + tmp = (ulist_t *)xmalloc(sizeof(ulist_t)); + + tmp->sys = xstrdup(tfn); + tmp->desc = NULL; + tmp->next = NULL; + + if (last) + last->next = tmp; + else + allowed_template_list_lhead = tmp; +} + +static void add_allowed_template_single(char *tfn) +{ + ulist_t *tmp, *last; + + if (!tfn || !*tfn) + return; + + tmp = last = allowed_template_single_lhead; + + while (tmp) { + if (!strcmp(tmp->sys, tfn)) + return; + last = tmp; + tmp = (ulist_t *)tmp->next; + } + + tmp = (ulist_t *)xmalloc(sizeof(ulist_t)); + + tmp->sys = xstrdup(tfn); + tmp->desc = NULL; + tmp->next = NULL; + + if (last) + last->next = tmp; + else + allowed_template_single_lhead = tmp; +} + /* called for fatal errors in parseconf like malloc failures */ static void upsstats_hosts_err(const char *errmsg) { upslogx(LOG_ERR, "Fatal error in parseconf(hosts.conf): %s", errmsg); } -static void load_hosts_conf(void) +static void load_hosts_conf(int handle_MONITOR) { char fn[NUT_PATH_MAX + 1]; PCONF_CTX_t ctx; @@ -1335,18 +1441,29 @@ static void load_hosts_conf(void) continue; } + if (ctx.numargs < 2) + continue; + + /* CUSTOM_TEMPLATE_LIST */ + if (!strcmp(ctx.arglist[0], "CUSTOM_TEMPLATE_LIST")) + add_allowed_template_list(ctx.arglist[1]); + + /* CUSTOM_TEMPLATE_SINGLE */ + if (!strcmp(ctx.arglist[0], "CUSTOM_TEMPLATE_SINGLE")) + add_allowed_template_single(ctx.arglist[1]); + if (ctx.numargs < 3) continue; /* MONITOR */ - if (!strcmp(ctx.arglist[0], "MONITOR")) + if (handle_MONITOR && !strcmp(ctx.arglist[0], "MONITOR")) add_ups(ctx.arglist[1], ctx.arglist[2]); } pconf_finish(&ctx); - if (!ulhead) { + if (!ulhead && handle_MONITOR) { /* Don't print HTML here if we are in JSON mode. * The JSON function will handle the error. */ @@ -1386,7 +1503,7 @@ static void display_single(void) if (treemode) display_tree(1); else - display_template(template_single); + display_template(template_single, 1); upscli_disconnect(&ups); upsdebug_call_finished0(); @@ -1428,7 +1545,7 @@ static void display_json(void) add_ups(monhost, monhostdesc); currups = ulhead; } else { - load_hosts_conf(); /* This populates ulhead */ + load_hosts_conf(1); /* This populates ulhead */ currups = ulhead; } @@ -1576,9 +1693,6 @@ int main(int argc, char **argv) nut_debug_level = i; } - /* Built-in defaults */ - template_single = xstrdup("upsstats-single.html"); - template_list = xstrdup("upsstats.html"); #ifdef NUT_CGI_DEBUG_UPSSTATS # if (NUT_CGI_DEBUG_UPSSTATS - 0 < 1) @@ -1597,6 +1711,10 @@ int main(int argc, char **argv) printf("

NUT CGI Debugging enabled, level: %d

\n\n", nut_debug_level); } + /* Built-in defaults */ + template_single = xstrdup("upsstats-single.html"); + template_list = xstrdup("upsstats.html"); + extractcgiargs(); upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); @@ -1634,14 +1752,18 @@ int main(int argc, char **argv) printf("Pragma: no-cache\n"); printf("\n"); - /* if a host is specified, use upsstats-single.html instead */ + /* if a host is specified, use upsstats-single.html instead + * of listing whatever we know about with upsstats.html */ + add_allowed_template_single("upsstats-single.html"); + add_allowed_template_list("upsstats.html"); if (monhost) { + load_hosts_conf(0); display_single(); } else { /* default: multimon replacement mode */ - load_hosts_conf(); + load_hosts_conf(1); currups = ulhead; - display_template(template_list); + display_template(template_list, 2); } /* Clean up memory */ @@ -1659,5 +1781,22 @@ int main(int argc, char **argv) free(template_single); free(template_list); + /* Free storage of allowed template names (reusing same kind of structure as UPSes) */ + while (allowed_template_single_lhead) { + currups = (ulist_t *)allowed_template_single_lhead->next; + free(allowed_template_single_lhead->sys); + free(allowed_template_single_lhead->desc); + free(allowed_template_single_lhead); + allowed_template_single_lhead = currups; + } + + while (allowed_template_list_lhead) { + currups = (ulist_t *)allowed_template_list_lhead->next; + free(allowed_template_list_lhead->sys); + free(allowed_template_list_lhead->desc); + free(allowed_template_list_lhead); + allowed_template_list_lhead = currups; + } + return 0; } diff --git a/conf/hosts.conf.sample b/conf/hosts.conf.sample index 37e561e78b..ef9fb8b525 100644 --- a/conf/hosts.conf.sample +++ b/conf/hosts.conf.sample @@ -9,8 +9,21 @@ # ----------------------------------------------------------------------- # # upsstats will use the list of MONITOR entries when displaying the -# default template (upsstats.html). The "FOREACHUPS" directive in the -# template will use this file to find systems running upsd. +# list template (default upsstats.html). The "FOREACHUPS" directive +# in the template will use this file to find systems running upsd. +# +# upsstats allows to use custom HTML template files for some of its +# outputs (JSON and "treemode" mark-ups are currently hard-coded in +# the binary), which you can specify in the query string part of the +# URI (in your `index.html` and/or added cells of `header.html`) as e.g. +# .../cgi-bin/upsstats.cgi?template_single=upsstats-custom-single.html&template_list=upsstats-custom-list.html +# Specific file names must be permitted below with `CUSTOM_TEMPLATE_LIST` +# or `CUSTOM_TEMPLATE_SINGLE` directive, as applicable; they must contain +# the `.htm` substring, and be located directly in the NUT configuration +# directory (same as default templates). If custom templates are used +# in the original request URI, they will be automatically suffixed to +# generated links (primarily HOSTLINK, but also just in case added to +# TREELINK and TREELINK_JSON). # # upsstats and upsimage also use this file to determine if a host may be # monitored. This keeps evil people from using your system to annoy @@ -30,3 +43,19 @@ # MONITOR myups@localhost "Local UPS" # MONITOR su2200@10.64.1.1 "Finance department" # MONITOR matrix@shs-server.example.edu "Sierra High School data room #1" + +# ----------------------------------------------------------------------- +# +# Allowed custom template file (adapted copy of upsstats.html) for listing +# your devices, which must be located in the same configuration directory +# and contain `.htm` in the file name. +# +# CUSTOM_TEMPLATE_LIST + +# ----------------------------------------------------------------------- +# +# Allowed custom template file (adapted copy of upsstats-single.html) for +# listing details about a single device, which must be located in the same +# configuration directory and contain `.htm` in the file name. +# +# CUSTOM_TEMPLATE_SINGLE diff --git a/docs/man/hosts.conf.txt b/docs/man/hosts.conf.txt index 940329b871..9f57e657d4 100644 --- a/docs/man/hosts.conf.txt +++ b/docs/man/hosts.conf.txt @@ -40,6 +40,26 @@ The description must be one element, so if it has spaces, then it must be wrapped with quotes as shown above. The default hostname is "localhost". +*CUSTOM_TEMPLATE_LIST* 'filename':: +*CUSTOM_TEMPLATE_SINGLE* 'filename':: + +linkman:upsstats[8] allows to use custom HTML template files for some of +its outputs (JSON and "treemode" mark-ups are currently hard-coded in +the binary), which you can specify in the query string part of the +URI (in your `index.html` and/or added cells of `header.html`) as e.g. +------ +.../cgi-bin/upsstats.cgi?template_single=custom-s.htm&template_list=custom-l.htm +------ ++ +Specific file names for a specific role must be permitted with these +directives (can be repeated). File names must contain the `.htm` sub-string +and must be located directly in the NUT configuration directory (same as +default templates). If custom templates are used in the original request +URI, they will be automatically suffixed to generated links (primarily +`HOSTLINK`, but also just in case added to `TREELINK` and `TREELINK_JSON`, +see linkman:upsstats.html[5] for more details). + + SEE ALSO -------- diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index 5665e28560..b30a37a03e 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -48,14 +48,24 @@ The web page that is displayed is actually a template containing commands to `upsstats` which are replaced by status information. The default file used for the overview of devices is `upsstats.html`. -An alternate template may be provided by `&template_list=...` CGI query option. +An alternate template may be provided by `&template_list=...` CGI +query option; relevant file must be allowed via `CUSTOM_TEMPLATE_LIST` +option in linkman:hosts.conf[5]. When monitoring a single UPS, the file displayed is `upsstats-single.html`. -An alternate template may be provided by `&template_single=...` CGI query option. +An alternate template may be provided by `&template_single=...` CGI +query option; relevant file must be allowed via `CUSTOM_TEMPLATE_SINGLE` +option in linkman:hosts.conf[5]. -Alternate templates must be structured similarly to default ones, located in the -same directory, and contain `.htm` in the file name. +Alternate templates must be structured similarly to default ones, +located in the same directory, and contain `.htm` in the file name. +You can specify in the query string part of the URI (for example, +in your local `index.html` and/or added cells of `header.html`) as e.g. + +------ +.../cgi-bin/upsstats.cgi?template_single=custom-s.html&template_list=custom-l.html +------ The format of these files, including the possible commands, is documented in linkman:upsstats.html[5]. @@ -88,7 +98,7 @@ parameter is also provided: In both modes, each UPS object includes: * *host*: The UPS identifier (e.g., "myups@localhost") -* *desc*: The host description from ``hosts.conf`` +* *desc*: The host description from `hosts.conf` * *status_raw*: The raw status string (e.g., "OL") * *status_parsed*: An array of human-readable status strings (e.g., ["Online"]) From 8b444e3ef61bebee58e01e76ff0e2b16febcb0be Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 9 Feb 2026 14:32:37 +0100 Subject: [PATCH 11/11] clients/upsstats.c: display_template(): if rejecting a file, or after other errors, use HTML
tags in the printout (especially if debug is on) [#2524] Signed-off-by: Jim Klimov --- clients/upsstats.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/clients/upsstats.c b/clients/upsstats.c index 0cfe4b3929..e1d310b192 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -1152,9 +1152,9 @@ static void display_template(const char *tfn, int type) if (!tfn || !*tfn || strstr(tfn, "/") || strstr(tfn, "\\")) { /* We only allow pre-configured templates in one managed location, with ".htm" in the name */ errno = EPERM; - fprintf(stderr, "upsstats: Can't open %s: %s: asked to look not exactly in the managed location\n", tfn, strerror(errno)); + fprintf(stderr, "upsstats: Can't open %s: %s: asked to look not exactly in the managed location
\n", tfn, strerror(errno)); - printf("Error: can't open template file (%s): Not authorized\n", tfn); + printf("Error: can't open template file (%s): Not authorized
\n", tfn); upsdebug_call_finished1(": subdir in template"); exit(EXIT_FAILURE); @@ -1163,9 +1163,9 @@ static void display_template(const char *tfn, int type) if (!strstr(tfn, ".htm")) { /* We only allow pre-configured templates with ".htm" in the name */ errno = EPERM; - fprintf(stderr, "upsstats: Can't open %s: %s: asked to look at not a *.htm* file\n", tfn, strerror(errno)); + fprintf(stderr, "upsstats: Can't open %s: %s: asked to look at not a *.htm* file
\n", tfn, strerror(errno)); - printf("Error: can't open template file (%s): Not authorized\n", tfn); + printf("Error: can't open template file (%s): Not authorized
\n", tfn); upsdebug_call_finished1(": not a *.htm* file"); exit(EXIT_FAILURE); @@ -1195,9 +1195,9 @@ static void display_template(const char *tfn, int type) if (!tmp) { /* We only allow pre-configured templates permitted via hosts.conf */ errno = EPERM; - fprintf(stderr, "upsstats: Can't open %s: %s: Not authorized: template not permitted via hosts.conf\n", tfn, strerror(errno)); + fprintf(stderr, "upsstats: Can't open %s: %s: Not authorized: template not permitted via hosts.conf
\n", tfn, strerror(errno)); - printf("Error: can't open template file (%s): Not authorized\n", tfn); + printf("Error: can't open template file (%s): Not authorized
\n", tfn); upsdebug_call_finished1(": template not permitted via hosts.conf"); exit(EXIT_FAILURE); @@ -1208,18 +1208,18 @@ static void display_template(const char *tfn, int type) tf = fopen(fn, "rb"); if (!tf) { - fprintf(stderr, "upsstats: Can't open %s: %s\n", fn, strerror(errno)); + fprintf(stderr, "upsstats: Can't open %s: %s
\n", fn, strerror(errno)); - printf("Error: can't open template file (%s)\n", tfn); + printf("Error: can't open template file (%s)
\n", tfn); upsdebug_call_finished1(": no template"); exit(EXIT_FAILURE); } if (!fgets(buf, sizeof(buf), tf)) { - fprintf(stderr, "upsstats: template file %s seems to be empty (fgets failed): %s\n", fn, strerror(errno)); + fprintf(stderr, "upsstats: template file %s seems to be empty (fgets failed): %s
\n", fn, strerror(errno)); - printf("Error: template file %s seems to be empty\n", tfn); + printf("Error: template file %s seems to be empty
\n", tfn); upsdebug_call_finished1(": empty template"); exit(EXIT_FAILURE); @@ -1229,9 +1229,9 @@ static void display_template(const char *tfn, int type) if (!strncmp(buf, "@NUT_UPSSTATS_TEMPLATE", 22)) { parse_line(buf); } else { - fprintf(stderr, "upsstats: template file %s does not start with NUT_UPSSTATS_TEMPLATE command\n", fn); + fprintf(stderr, "upsstats: template file %s does not start with NUT_UPSSTATS_TEMPLATE command
\n", fn); - printf("Error: template file %s does not start with NUT_UPSSTATS_TEMPLATE command\n", tfn); + printf("Error: template file %s does not start with NUT_UPSSTATS_TEMPLATE command
\n", tfn); upsdebug_call_finished1(": not a valid template"); exit(EXIT_FAILURE);