Merge branch 'master' of github.com:webmin/webmin

This commit is contained in:
Jamie Cameron
2026-09-29 17:46:39 -07:00
6 changed files with 127 additions and 4 deletions

View File

@@ -1,4 +1,8 @@
## Changelog
#### 2.671 (October, 2026)
* Fix two-factor authentication validation to use the Webmin configuration and data directories passed by the caller
* Fix HTML escaping of module, script and session fields on the Webmin Actions Log details page
#### 2.670 (September 20, 2026)
* Add new Hardware Information module for inspecting system, firmware, security, PCI, USB, storage, network, processor, sensor, driver, and kernel module details
* Add options to send Webmin and Usermin errors to the systemd journal [forum.virtualmin.com/t/136562](https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562)
@@ -20,6 +24,13 @@
* Fix ACL operations bypassing configured allowed paths in File Manager
* Fix arbitrary file reads in Software Packages module
* Fix TLS client certificate verification
* Fix Apache Webserver module to enforce the allowed directory ACL when manually editing `.htaccess` files
* Fix Custom Commands module to properly quote parameter values passed to commands and to reject parameters with multiple values
* Fix DNSSEC NSEC3 zone signing in BIND DNS Server module to use zero additional hash iterations, as recommended by RFC 9276
* Fix System Logs module to create a new log file only after the log entry passes validation
* Fix file chooser to lift the root directory restriction only when the chroot is at or under the allowed root
* Fix password changes from the login page using a configured password change command to fail unless the command prompts for the old password
* Fix File Manager search results to only include files under the allowed directories
* Update Backup Configuration module's destination selector to use the new select-based UI
* Update the Authentic theme to the latest version with various improvements:
- Add support for HTML signatures in Usermin

View File

@@ -314,7 +314,7 @@ password_enew2=Your new passwords do not match
password_epam=PAM error : $1
password_emodpam=The Authen::PAM perl module needed to do password changes is not installed!
password_enewpass=New password is not allowed : $1
password_enotold=Password change commit did not prompt for old password!
password_enotold=Password change command did not prompt for old password!
ui_mandatory=This field is mandatory
ui_checkmandatory=Nothing was selected

View File

@@ -585,10 +585,11 @@ my ($name) = @_;
return (0, $text{'systemd_ename'}) if (!valid_unit_name($name));
my $out = backquote_logged(
"systemctl start ".quotemeta($name)." 2>&1 </dev/null");
if ($? && $out =~ /journalctl/) {
my $rv = $?;
if ($rv && $out =~ /journalctl/) {
$out .= backquote_command("journalctl -xe 2>/dev/null");
}
return (!$?, $out);
return (!$rv, $out);
}
=head2 stop_unit(name)
@@ -661,6 +662,33 @@ my $out = backquote_logged(
return (!$?, $out);
}
=head2 get_unit_state(name)
Returns a fresh hash of LoadState, UnitFileState, ActiveState, SubState and
MainPID, followed by an error string. Missing units have LoadState not-found;
command or validation failures return undef and an error. No inventory cache
is used, so callers can verify a change immediately.
=cut
sub get_unit_state
{
my ($name) = @_;
return (undef, $text{'systemd_ename'}) if (!valid_unit_name($name));
my $out = backquote_logged("systemctl show --no-pager ".
"--property=LoadState,UnitFileState,ActiveState,SubState,MainPID ".
quotemeta($name)." 2>&1 </dev/null");
my $rv = $?;
my %state;
foreach my $line (split(/\r?\n/, $out)) {
$state{$1} = $2
if ($line =~ /^(LoadState|UnitFileState|ActiveState|SubState|MainPID)=(.*)$/);
}
# systemctl may return a nonzero status for an explicitly missing unit.
return (undef, $out || "Failed to read systemd unit $name")
if (!$state{'LoadState'} || ($rv && $state{'LoadState'} ne 'not-found'));
return (\%state, undef);
}
=head2 dependencies_unit(name)
Gets dependency tree output for a systemd unit.

View File

@@ -846,6 +846,39 @@ like(get_unit_root(), qr{^/(etc|usr/lib|lib)/systemd/system$},
'dependency command uses full non-paged output');
}
# Failed starts must remain failures even when journal diagnostics succeed.
{
local *main::backquote_logged = sub { $? = 256; return 'See journalctl for details'; };
local *main::backquote_command = sub { $? = 0; return 'Start failed'; };
my ($ok, $out) = start_unit('broken.service');
ok(!$ok, 'journal lookup cannot hide a failed start');
like($out, qr/Start failed/, 'failed start includes diagnostics');
}
# Readiness needs fresh properties, including idle sockets and missing units.
{
my $reply = "LoadState=loaded\nUnitFileState=enabled\nActiveState=active\nSubState=listening\nMainPID=0\n";
my $status = 0;
my @commands;
local *main::backquote_logged = sub { push @commands, $_[0]; $? = $status; return $reply; };
my ($state, $error) = get_unit_state('demo.socket');
is($error, undef, 'valid unit state has no error');
is($state->{SubState}, 'listening', 'socket readiness is exposed');
like($commands[-1], qr/--property=LoadState,UnitFileState,ActiveState,SubState,MainPID/, 'only requested properties are read');
$reply = "LoadState=loaded\nUnitFileState=enabled\nActiveState=inactive\n";
($state, $error) = get_unit_state('demo.socket');
is($state->{ActiveState}, 'inactive', 'state is not cached');
$reply = "LoadState=not-found\nActiveState=inactive\n"; $status = 1024;
($state, $error) = get_unit_state('missing.service');
is($state->{LoadState}, 'not-found', 'missing unit is a valid discovery result');
$reply = 'Failed to connect to bus';
($state, $error) = get_unit_state('demo.socket');
ok(!defined($state), 'bus failure returns no state');
like($error, qr/connect to bus/, 'bus error is preserved');
($state, $error) = get_unit_state('bad;unit.service');
is($error, 'bad unit name', 'invalid state lookup is rejected');
}
{
my @cmds;
my $reloaded = 0;

View File

@@ -0,0 +1,49 @@
#!/usr/bin/perl
# Concurrent WebSocket updates must not reuse a same-second config snapshot.
use strict;
use warnings;
use Test::More;
use File::Temp qw(tempdir);
use File::Basename qw(dirname);
use File::Spec;
require File::Spec->rel2abs(dirname(__FILE__).'/../web-lib-funcs.pl');
my $directory = tempdir(CLEANUP => 1);
my $file = "$directory/miniserv.conf";
{
no warnings qw(redefine once);
*main::get_miniserv_config_file = sub { return $file; };
}
# replace_config(contents) simulates another process writing within one second.
sub replace_config
{
my ($contents) = @_;
open(my $fh, '>', $file) or die $!;
print {$fh} $contents;
close($fh) or die $!;
utime(1700000000, 1700000000, $file) or die $!;
}
replace_config("port=10000\nwebsockets_/test/ws-555=first\n");
my %first;
ok(main::get_miniserv_config(\%first), 'read initial configuration');
is($first{'websockets_/test/ws-555'}, 'first', 'first route is present');
# Allocation must retain a route added since the caller's initial config read.
replace_config("port=10000\nwebsockets_/test/ws-555=first\n".
"websockets_/test/ws-556=second\n");
my %added;
main::get_miniserv_config(\%added);
is($added{'websockets_/test/ws-556'}, 'second',
'same-second route addition is visible');
# Cleanup must not restore a route another backend has already removed.
replace_config("port=10000\nwebsockets_/test/ws-556=second\n");
my %removed;
main::get_miniserv_config(\%removed);
ok(!exists($removed{'websockets_/test/ws-555'}),
'same-second route removal is visible');
is($removed{'websockets_/test/ws-556'}, 'second',
'unrelated route survives cleanup');
done_testing();

View File

@@ -3083,7 +3083,9 @@ hash reference.
=cut
sub get_miniserv_config
{
return &read_file_cached_with_stat(&get_miniserv_config_file(), $_[0]);
# WebSocket backends can change routes several times within one second.
# An mtime-based cache can lose another process's update even under a file lock.
return &read_file(&get_miniserv_config_file(), $_[0]);
}
=head2 put_miniserv_config(&hash)