Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8f789d4
Consider EXEEXT in CGI programs, templates and docs [#3207]
jimklimov Jan 6, 2026
4111e87
docs/man/upsset.conf.txt, docs/nut.dict, NEWS.adoc: documented setup …
jimklimov Jan 6, 2026
1999175
appveyor.yml: comment about "mingwXX" in archived N4W builds [#3207]
jimklimov Jan 6, 2026
f95c75a
docs/man/upsset.conf.txt: revise MS IIS setup for NUT CGI experimenta…
jimklimov Jan 6, 2026
2221237
clients/upsstats.c: use the more dynamic confpath() rather than built…
jimklimov Jan 6, 2026
dce9375
drivers/snmp-ups.c, docs/man/snmp-ups.txt: use the more dynamic confp…
jimklimov Jan 6, 2026
95b3fde
tools/nut-scanner/scan_nut_simulation.c, NEWS.adoc: use the more dyna…
jimklimov Jan 6, 2026
d4cb9c3
docs/man/upsset.conf.txt: note also the "html" directory for pretty U…
jimklimov Jan 6, 2026
0fe60a5
clients/ups{set,stats,image}.c: support NUT_DEBUG_LEVEL envvar in CGI…
jimklimov Jan 6, 2026
64d2607
configure.ac, data/htmlcgi/header.html.in, docs/configure.txt, docs/m…
jimklimov Jan 6, 2026
1de5a7f
common/parseconf.c: pconf_file_begin(): extend limited UNC path suppo…
jimklimov Jan 7, 2026
0db2f37
clients/upsset.c, clients/upsstats.c: debug-trace which config file w…
jimklimov Jan 7, 2026
e08d7f1
common/common.c, common/parseconf.c: generalize the WIN32 UNC localho…
jimklimov Jan 7, 2026
1a991ac
scripts/Windows/wininit.c: err on the safe side and allocate buffers …
jimklimov Jan 7, 2026
f7d869a
NEWS.adoc, docs/nut.dict: update and restructure news for 2.8.5 cycle
jimklimov Jan 7, 2026
9b23709
Convert conf/upsstats*.html.sample files into .in templates to consid…
jimklimov Jan 7, 2026
f6e06ff
docs/man/ups*.{cgi,html}.txt: refer to upsset.conf page about web ser…
jimklimov Jan 7, 2026
71820cc
docs/man/upsset.conf.txt: suggest more ways of hiding/securing NUT CG…
jimklimov Jan 7, 2026
2d2a3b5
docs/man/upsstats.html.txt: mention sibling upsstats-single.html earl…
jimklimov Jan 7, 2026
cfe56fe
data/htmlcgi/bottom.html: populate with a bit of text so browser user…
jimklimov Jan 7, 2026
8aa7d7b
data/htmlcgi/index.html, data/htmlcgi/Makefile.am, NEWS.adoc: use a N…
jimklimov Jan 7, 2026
f2fc5b5
clients/ups{set,stats,image}.c: treat stdout as a binary stream for C…
jimklimov Jan 7, 2026
270ebf9
clients/upsset.c: on WIN32 always extractpostargs() [#3207]
jimklimov Jan 7, 2026
f79ad22
clients/ups{set,stats,image}.c: setmode() actually seems to not be wi…
jimklimov Jan 7, 2026
6ae3cd9
clients/upsset.c, clients/cgilib.c: move the time-constrained read of…
jimklimov Jan 7, 2026
17a2bc3
clients/cgilib.c: extractpostargs(): troubleshoot incoming bytes [#3219]
jimklimov Jan 7, 2026
2ed64f0
clients/ups{set,stats,image}.c: on WIN32 initialize socket layer for …
jimklimov Jan 7, 2026
6ca0fd5
clients/cgilib.c: extractpostargs(): on WIN32, select() is a SOCKET c…
jimklimov Jan 7, 2026
f0e95df
clients/cgilib.c: extractpostargs(): revise debug logging [#3249]
jimklimov Jan 8, 2026
a6eb302
clients/upsset.c, clients/cgilib.c: in cgilib, see if server tells us…
jimklimov Jan 8, 2026
bddf145
clients/cgilib.c: extractpostargs(): simplify the loop to avoid poten…
jimklimov Jan 8, 2026
4a05717
docs/man/upsset.conf.txt: document recommendations about CGI timeout …
jimklimov Jan 8, 2026
3a77fde
NUT CGI upsstats (prog, doc, template): add links to expose device/al…
jimklimov Jan 8, 2026
88c5f8b
clients/upsstats.c: add call-stack tracing [#3249]
jimklimov Jan 10, 2026
f2c33c4
clients/upsstats.c: display_template(): open HTML template in binary …
jimklimov Jan 10, 2026
4656189
clients/upsstats.c, docs/man/upsstats.html.txt, conf/upsstats*.html.s…
jimklimov Jan 10, 2026
50ade3a
docs/man/upsset.conf.txt: revise recommendations for CGI engine timeo…
jimklimov Jan 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 118 additions & 110 deletions NEWS.adoc

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions UPGRADING.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ Changes from 2.8.4 to 2.8.5
installed (and possibly customized) with the `*.html.sample` files delivered
by the new build. [PR #3180]

- Introduced a `@NUT_UPSSTATS_TEMPLATE@` command which the NUT CGI template
files now MUST start with (safety check that we are reading a template).
While the delivered `upsstats*.html.sample` files would include the change,
ultimate `upsstats*.html` templates deployed for end-users MUST be updated.
[issue #3252, PR #3249]

- Dropped the `compile` script from Git sources. It originates from automake
and is added to work area (if missing) during `autogen.sh` rituals anyway
(as `make` says, `'automake --add-missing' can install 'compile'` when you
Expand Down
9 changes: 9 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ build_script:
set MSYSTEM=MINGW64
REM Note: currently we save job time and do not install asciidoc/a2x
REM # --with-docs="man=auto html-single=auto html-chunked=no pdf=no"
REM The resulting configuration defaults to --prefix=/mingw{32,64}
REM as appropriate for the platform, which ends up as a subdirectory
REM under DESTDIR when we `make install` later, and in our 7z archive.
REM The builds are relocatable however, so end-users can extract to
REM e.g. "C:\Program Files\NUT" and forfeit "mingwXX" part, probably,
REM but this can complicate automatic relative directory resolution
REM to find "nearby" program or configuration files (see common.c
REM for current implementation). Hard-coded fallback strings may
REM end up getting used in those cases.
C:\msys64\usr\bin\bash -lc 'date -u; PATH="/mingw64/bin:$PATH" CI_SKIP_CHECK=true CANBUILD_WITH_LIBMODBUS_USB=yes ./ci_build.sh --with-docs=no'


Expand Down
116 changes: 97 additions & 19 deletions clients/cgilib.c
Original file line number Diff line number Diff line change
Expand Up @@ -116,42 +116,120 @@ void extractcgiargs(void)

void extractpostargs(void)
{
char buf[SMALLBUF], *ptr, *cleanval;
int ch;
char buf[SMALLBUF], *ptr, *cleanval, *server_software = NULL;
int ch, content_length = -1, bytes_seen = 0;
size_t buflen;

/* First, see if there's anything waiting...
* the server may not close STDIN properly
* or somehow delay opening/populating it. */
#ifndef WIN32
int selret;
fd_set fds;
struct timeval tv;

FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
tv.tv_sec = 0;
tv.tv_usec = 250000; /* wait for up to 250ms for a POST query to come */

selret = select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
if (selret <= 0) {
#else
HANDLE hSTDIN = GetStdHandle(STD_INPUT_HANDLE);
DWORD selret = WaitForSingleObject(hSTDIN, 250);
if (selret != WAIT_OBJECT_0) { /* or == WAIT_TIMEOUT ? */
#endif /* WIN32 */
upsdebug_with_errno(1, "%s: no stdin is waiting (%" PRIiMAX ")<br/>", __func__, (intmax_t)selret);
return;
}

ch = fgetc(stdin);
buf[0] = '\0';

while (ch != EOF) {
if (ch == '&') {
/* Does the web server tell us how much it sent
* (and might keep the channel open... indefinitely)? */
ptr = getenv("CONTENT_LENGTH");
if (ptr) {
content_length = atoi(ptr);
}

ptr = getenv("SERVER_SOFTWARE");
if (ptr) {
server_software = ptr;
} else {
server_software = "";
}

if (content_length > 0 && strstr(server_software, "IIS")) {
/* Our POSTs end with a newline, and that one never arrives
* (reads hang), possibly buffered output from IIS?
* Our own setmode() in e.g. upsset.c does not help.
* So upsset.c ends each FORM with do_hidden_sentinel()
* to sacrifice a few bytes we would not use.
*/
upsdebugx(3, "%s: truncating expected content length on IIS<br/>", __func__);
content_length--;
}
upsdebugx(3, "%s: starting to read %d POSTed bytes on server '%s'<br/>", __func__, content_length, server_software);

ch = fgetc(stdin);
upsdebugx(6, "%s: got char: '%c' (%d, 0x%02X)<br/>", __func__, ch, ch, (unsigned int)ch);

if (ch == EOF) {
bytes_seen++;
upsdebugx(3, "%s: got immediate EOF in stdin<br/>", __func__);
} else while(1) {
bytes_seen++;
if (ch == '&' || ch == EOF || (content_length >= 0 && bytes_seen >= content_length)) {
buflen = strlen(buf);
upsdebugx(1, "%s: collected a chunk of %" PRIuSIZE " bytes on stdin: %s<br/>",
__func__, buflen, buf);
ptr = strchr(buf, '=');
if (!ptr)
if (!ptr) {
upsdebugx(3, "%s: parsearg('%s', '')<br/>", __func__, buf);
parsearg(buf, "");
else {
} else {
*ptr++ = '\0';
cleanval = unescape(ptr);
upsdebugx(3, "%s: parsearg('%s', '%s')<br/>", __func__, buf, cleanval);
parsearg(buf, cleanval);
free(cleanval);
}
buf[0] = '\0';

if (ch == EOF || (content_length >= 0 && bytes_seen >= content_length))
break; /* end the loop */
}
else
snprintfcat(buf, sizeof(buf), "%c", ch);

#ifndef WIN32
/* Must re-init every time when looping (array is changed by select method) */
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
tv.tv_sec = 0;
tv.tv_usec = 250000; /* wait for up to 250ms for a POST response */

selret = select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
if (selret <= 0) {
#else
selret = WaitForSingleObject(hSTDIN, 250);
if (selret != WAIT_OBJECT_0) { /* or == WAIT_TIMEOUT ? */
#endif
/* We do not always get EOF, so assume the input stream stopped */
upsdebug_with_errno(1, "%s: timed out waiting for an stdin byte (%" PRIiMAX ")<br/>", __func__, (intmax_t)selret);
break;
}

fflush(stderr);
ch = fgetc(stdin);
}
upsdebugx(6, "%s: got char: '%c' (%d, 0x%02X)<br/>", __func__, ch, ch, (unsigned int)ch);
if (ch == EOF)
upsdebugx(3, "%s: got proper stdin EOF<br/>", __func__);
upsdebugx(6, "%s: processed %d bytes with %d expected incoming content length on server '%s'<br/>", __func__, bytes_seen, content_length, server_software);
} /* end of infinite loop */

if (strlen(buf) != 0) {
ptr = strchr(buf, '=');
if (!ptr)
parsearg(buf, "");
else {
*ptr++ = '\0';
cleanval = unescape(ptr);
parsearg(buf, cleanval);
free(cleanval);
}
}
upsdebugx(3, "%s: processed %d bytes with %d incoming content length<br/>", __func__, bytes_seen, content_length);
}

/* called for fatal errors in parseconf like malloc failures */
Expand Down
28 changes: 27 additions & 1 deletion clients/upsimage.c
Original file line number Diff line number Diff line change
Expand Up @@ -614,12 +614,38 @@ static int get_var(const char *var, char *buf, size_t buflen)

int main(int argc, char **argv)
{
char str[SMALLBUF];
char str[SMALLBUF], *s;
int i, min, nom, max;
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;
}

/* Avoid binary output conversions, e.g.
* mangling what looks like CRLF on WIN32 */
setmode(STDOUT_FILENO, O_BINARY);
#endif

NUT_UNUSED_VARIABLE(argc);
NUT_UNUSED_VARIABLE(argv);

/* NOTE: Caller must `export NUT_DEBUG_LEVEL` to see debugs for upsc
* and NUT methods called from it. This line aims to just initialize
* the subsystem, and set initial timestamp. Debugging the client is
* primarily of use to developers, so is not exposed via `-D` args.
*/
s = getenv("NUT_DEBUG_LEVEL");
if (s && str_to_int(s, &i, 10) && i > 0) {
nut_debug_level = i;
}

extractcgiargs();

upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT);
Expand Down
66 changes: 50 additions & 16 deletions clients/upsset.c
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ static void do_hidden(const char *next)
next);
}

static void do_hidden_sentinel(void)
{
/* MS IIS tends to not close CGI STDIN and not serve the last byte(s)
* but just hangs at fgets(), so we truncate the inputs in cgilib,
* and add a dummy entry here that we can afford to lose in the end */
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"zzz\" VALUE=\"sentinel\">\n");
}

/* generate SELECT chooser from hosts.conf entries */
static void upslist_arg(size_t numargs, char **arg)
{
Expand Down Expand Up @@ -202,7 +210,7 @@ static void do_pickups(const char *currfunc)

snprintf(hostfn, sizeof(hostfn), "%s/hosts.conf", confpath());

printf("<FORM METHOD=\"POST\" ACTION=\"upsset.cgi\">\n");
printf("<FORM METHOD=\"POST\" ACTION=\"upsset.cgi" EXEEXT "\">\n");

printf("Select UPS and function:\n<BR>\n");

Expand Down Expand Up @@ -258,6 +266,8 @@ static void do_pickups(const char *currfunc)
do_hidden(NULL);

printf("<INPUT TYPE=\"SUBMIT\" VALUE=\"View\">\n");

do_hidden_sentinel();
printf("</FORM>\n");
}

Expand Down Expand Up @@ -315,7 +325,7 @@ static void loginscreen(void)
static void loginscreen(void)
{
do_header("Login");
printf("<FORM METHOD=\"POST\" ACTION=\"upsset.cgi\">\n");
printf("<FORM METHOD=\"POST\" ACTION=\"upsset.cgi" EXEEXT "\">\n");
start_table();

printf("<TR BGCOLOR=\"#60B0B0\">\n");
Expand All @@ -332,6 +342,7 @@ static void loginscreen(void)
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"function\" VALUE=\"pickups\">\n");
printf("<INPUT TYPE=\"SUBMIT\" VALUE=\"Login\">\n");
printf("<INPUT TYPE=\"RESET\" VALUE=\"Reset fields\">\n");
do_hidden_sentinel();
printf("</TD></TR></TABLE>\n");
printf("</FORM>\n");
printf("</TD></TR></TABLE>\n");
Expand Down Expand Up @@ -445,7 +456,7 @@ static void showcmds(void)
"This UPS doesn't support any instant commands.");

do_header("Instant commands");
printf("<FORM ACTION=\"upsset.cgi\" METHOD=\"POST\">\n");
printf("<FORM METHOD=\"POST\" ACTION=\"upsset.cgi" EXEEXT "\">\n");
start_table();

/* include the description from checkhost() if present */
Expand Down Expand Up @@ -483,6 +494,7 @@ static void showcmds(void)
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"monups\" VALUE=\"%s\">\n", monups);
printf("<INPUT TYPE=\"SUBMIT\" VALUE=\"Issue command\">\n");
printf("<INPUT TYPE=\"RESET\" VALUE=\"Reset\">\n");
do_hidden_sentinel();
printf("</TD></TR>\n");
printf("</TABLE>\n");
printf("</FORM>\n");
Expand Down Expand Up @@ -873,7 +885,7 @@ static void showsettings(void)
}

do_header("Current settings");
printf("<FORM ACTION=\"upsset.cgi\" METHOD=\"POST\">\n");
printf("<FORM METHOD=\"POST\" ACTION=\"upsset.cgi" EXEEXT "\">\n");
start_table();

/* include the description from checkhost() if present */
Expand Down Expand Up @@ -905,6 +917,7 @@ static void showsettings(void)
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"monups\" VALUE=\"%s\">\n", monups);
printf("<INPUT TYPE=\"SUBMIT\" VALUE=\"Save changes\">\n");
printf("<INPUT TYPE=\"RESET\" VALUE=\"Reset\">\n");
do_hidden_sentinel();
printf("</TD></TR>\n");
printf("</TABLE>\n");
printf("</FORM>\n");
Expand Down Expand Up @@ -1043,6 +1056,7 @@ static void check_conf(void)
PCONF_CTX_t ctx;

snprintf(fn, sizeof(fn), "%s/upsset.conf", confpath());
upsdebugx(1, "%s: considering configuration file %s", __func__, fn);

pconf_init(&ctx, upsset_conf_err);

Expand Down Expand Up @@ -1092,30 +1106,50 @@ static void check_conf(void)

int main(int argc, char **argv)
{
char *s;
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;
}

/* Avoid binary output conversions, e.g.
* mangling what looks like CRLF on WIN32 */
setmode(STDOUT_FILENO, O_BINARY);
/* Also do not break what we receive from HTTP POST queries */
setmode(STDIN_FILENO, O_BINARY);
#endif

NUT_UNUSED_VARIABLE(argc);
NUT_UNUSED_VARIABLE(argv);
username = password = function = monups = NULL;

printf("Content-type: text/html\n\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
* the subsystem, and set initial timestamp. Debugging the client is
* primarily of use to developers, so is not exposed via `-D` args.
*/
s = getenv("NUT_DEBUG_LEVEL");
if (s && str_to_int(s, &i, 10) && i > 0) {
nut_debug_level = i;
}

/* see if the magic string is present in the config file */
check_conf();

upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT);

/* see if there's anything waiting .. the server my not close STDIN properly */
if (1) {
fd_set fds;
struct timeval tv;
extractpostargs();

FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
tv.tv_sec = 0;
tv.tv_usec = 250000; /* wait for up to 250ms for a POST response */

if ((select(STDIN_FILENO+1, &fds, 0, 0, &tv)) > 0)
extractpostargs();
}
/* Nothing POSTed (or parsed correctly)? */
if ((!username) || (!password) || (!function))
loginscreen();

Expand Down
Loading
Loading