mirror of
https://github.com/webmin/webmin.git
synced 2026-10-23 20:50:39 +01:00
This patch fixes missing SSH logins on systems without `utmp`, including Ubuntu 26.04. It adds a Systemd session helper, excludes manager and Webmin and Usermin sessions keeping `who` as the fallback and existing backend where `utmp` is present. This complements mine earlier https://github.com/webmin/webmin/commit/8417d5c patch, which fixed parsing but could not get sessions missing from `who` output. Tested on Ubuntu 26.04 Pro and AlmaLinux 10 Pro with both local live test and suites pass.
228 lines
6.6 KiB
Prolog
Executable File
228 lines
6.6 KiB
Prolog
Executable File
# linux-lib.pl
|
|
# Functions for reading linux format last output
|
|
|
|
# passfiles_type()
|
|
# Returns 0 for old-style passwords (/etc/passwd only), 1 for FreeBSD-style
|
|
# (/etc/master.passwd) and 2 for SysV (/etc/passwd & /etc/shadow)
|
|
sub passfiles_type
|
|
{
|
|
return &password_file($config{'shadow_file'}) ? 2 : 0;
|
|
}
|
|
|
|
# groupfiles_type()
|
|
# Returns 0 for normal group file (/etc/group only) and 2 for shadowed
|
|
# (/etc/group and /etc/gshadow)
|
|
sub groupfiles_type
|
|
{
|
|
return &password_file($config{'gshadow_file'}) ? 2 : 0;
|
|
}
|
|
|
|
# open_last_command(handle, user, [max])
|
|
sub open_last_command
|
|
{
|
|
my ($fh, $user, $max) = @_;
|
|
if (&has_command("last")) {
|
|
my $quser = quotemeta($user);
|
|
$max = " -n ".quotemeta($max) if ($max);
|
|
open($fh, "(last -F -w$max $quser || last -w$max $quser) |");
|
|
}
|
|
else {
|
|
open($fh, "</dev/null");
|
|
}
|
|
}
|
|
|
|
# read_last_line(handle)
|
|
# Parses a line of output from last into an array of
|
|
# user, tty, host, login, logout, period
|
|
sub read_last_line
|
|
{
|
|
$fh = $_[0];
|
|
while(1) {
|
|
chop($line = <$fh>);
|
|
if (!$line) { return (); }
|
|
if ($line =~ /system boot/) { next; }
|
|
if ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+)\s+\-\s+(\S+)\s+\((\d+:\d+)\)/) {
|
|
# jcameron pts/0 fudu Thu Feb 22 09:47 - 10:15
|
|
return ($1, $2, $3, $4, $5 eq "down" ? "Shutdown" : $5, $6);
|
|
}
|
|
elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+\-\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+\((.*?\d+:\d+)\)/) {
|
|
# jcameron pts/0 fudu Thu Feb 22 09:47 - 10:15
|
|
# jcameron pts/0 fudu Sun Feb 4 02:26:28 2024 - Wed Feb 7 18:25:09 2024 (3+15:58)
|
|
return ($1, $2, $3, $4, $5 eq "down" ? "Shutdown" : $5, $6);
|
|
}
|
|
elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+)\s+(still|gone)/ ||
|
|
$line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(still|gone)/) {
|
|
# root pts/0 fudu Fri Feb 23 18:46 still logged in
|
|
# root ftpd10 fudu Thu Jun 20 11:19 gone - no logout
|
|
# root pts/0 fudu Tue Jun 18 23:10:30 2024 still logged in
|
|
return ($1, $2, $3, $4);
|
|
}
|
|
}
|
|
}
|
|
|
|
# os_most_recent_logins()
|
|
# Returns hash ref from username to the most recent login as time string
|
|
sub os_most_recent_logins
|
|
{
|
|
my %rv;
|
|
&clean_language();
|
|
open(LASTLOG, "lastlog |");
|
|
while(<LASTLOG>) {
|
|
s/\r|\n//g;
|
|
if (/^(\S+)/) {
|
|
my $user = $1;
|
|
if (/((\S+)\s+(\S+)\s+\d+\s+(\d+):(\d+):(\d+)\s+([\-\+]\d+)\s+(\d+))/) {
|
|
# Have a date to parse
|
|
$rv{$user} = $1;
|
|
}
|
|
else {
|
|
$rv{$user} = undef;
|
|
}
|
|
}
|
|
}
|
|
close(LASTLOG);
|
|
&reset_environment();
|
|
return \%rv;
|
|
}
|
|
|
|
# logged_in_users()
|
|
# Returns a list of login hashes with user, tty, when and from fields.
|
|
sub logged_in_users
|
|
{
|
|
local @rv;
|
|
|
|
# Use logind when utmp is absent and systemd and its Webmin module are available.
|
|
if (!-e "/run/utmp" && !-e "/var/run/utmp" &&
|
|
-d "/run/systemd/system" && &foreign_check("systemd")) {
|
|
&foreign_require("systemd");
|
|
my $sessions = &systemd::list_login_sessions();
|
|
# Accept a successful result even when it contains no sessions.
|
|
if (defined($sessions)) {
|
|
foreach my $s (@$sessions) {
|
|
# Keep terminal/desktop types, including SSH without a
|
|
# TTY. Exclude managers, closing sessions and web
|
|
# logins. Webmin/Usermin use a port as the TTY, even
|
|
# with custom PAM names.
|
|
next if ($s->{'Class'} !~ /^user(?:-|$)/ ||
|
|
$s->{'Type'} !~ /^(tty|x11|wayland)$/ ||
|
|
$s->{'Service'} =~ /^(webmin|usermin)$/ ||
|
|
$s->{'TTY'} =~ /^\d+$/ ||
|
|
$s->{'State'} eq "closing");
|
|
|
|
# Match the existing date format; use the service if TTY
|
|
# is absent.
|
|
my ($when) = $s->{'Timestamp'} =~
|
|
/(\d+-\d+-\d+\s+\d+:\d+)/;
|
|
push(@rv, { 'user' => $s->{'Name'},
|
|
'tty' => $s->{'TTY'} || $s->{'Service'},
|
|
'when' => $when,
|
|
'from' => $s->{'RemoteHost'} });
|
|
}
|
|
return @rv;
|
|
}
|
|
}
|
|
|
|
# Use who when utmp exists, logind is unavailable, or its query fails.
|
|
open(WHO, "who |");
|
|
while(<WHO>) {
|
|
# root pts/0 Oct 15 02:33 (192.168.1.1) C locale
|
|
# root pts/0 2026-10-15 02:33 (192.168.1.1) other locales
|
|
# root sshd pts/0 2026-10-15 02:33 (192.168.1.1) logind backend
|
|
if (/^(\S+)\s+(\S.*?)\s+(\S+\s+\d+\s+\d+:\d+|\d+-\d+-\d+\s+\d+:\d+)\s+(\((\S+)\))?/) {
|
|
push(@rv, { 'user' => $1, 'tty' => $2,
|
|
'when' => $3, 'from' => $5 });
|
|
}
|
|
}
|
|
close(WHO);
|
|
return @rv;
|
|
}
|
|
|
|
# use_md5()
|
|
# Returns 1 if pam is set up to use MD5 encryption, 2 for blowfish, 3 for SHA512
|
|
sub use_md5
|
|
{
|
|
if (defined($use_md5_cache)) {
|
|
# Don't re-look this up
|
|
return $use_md5_cache;
|
|
}
|
|
local $md5 = 0;
|
|
if (&foreign_check("pam")) {
|
|
# Use the PAM module if we can
|
|
&foreign_require("pam", "pam-lib.pl");
|
|
local @conf = &foreign_call("pam", "get_pam_config");
|
|
local ($svc) = grep { $_->{'name'} eq 'passwd' } @conf;
|
|
LOOP:
|
|
foreach my $m (@{$svc->{'mods'}}) {
|
|
if ($m->{'type'} eq 'password') {
|
|
if ($m->{'args'} =~ /md5/) {
|
|
$md5 = 1;
|
|
}
|
|
elsif ($m->{'args'} =~ /yescrypt/) {
|
|
$md5 = 4;
|
|
}
|
|
elsif ($m->{'args'} =~ /sha512/) {
|
|
$md5 = 3;
|
|
}
|
|
elsif ($m->{'args'} =~ /blowfish/) {
|
|
$md5 = 2;
|
|
}
|
|
elsif ($m->{'module'} =~ /pam_stack\.so/ &&
|
|
$m->{'args'} =~ /service=(\S+)/) {
|
|
# Referred to another service!
|
|
($svc) = grep { $_->{'name'} eq $1 } @conf;
|
|
if ($svc) { goto LOOP }
|
|
else { last; }
|
|
}
|
|
elsif ($m->{'control'} eq 'include') {
|
|
# Include another section
|
|
($svc) = grep { $_->{'name'} eq $m->{'module'} }
|
|
@conf;
|
|
if ($svc) { goto LOOP }
|
|
else { last; }
|
|
}
|
|
}
|
|
elsif ($m->{'include'}) {
|
|
# Include another section, with @ syntax
|
|
($svc) = grep { $_->{'name'} eq $m->{'include'} } @conf;
|
|
if ($svc) { goto LOOP }
|
|
else { last; }
|
|
}
|
|
}
|
|
}
|
|
if (!$md5 && &open_readfile(PAM, "/etc/pam.d/passwd")) {
|
|
# Otherwise try to check the PAM file directly
|
|
while(<PAM>) {
|
|
s/#.*$//g;
|
|
if (/^password.*md5/) { $md5 = 1; }
|
|
elsif (/^password.*blowfish/) { $md5 = 2; }
|
|
elsif (/^password.*sha512/) { $md5 = 3; }
|
|
elsif (/^password.*yescrypt/) { $md5 = 4; }
|
|
}
|
|
close(PAM);
|
|
}
|
|
if (!$md5 && (&open_readfile(PAM, "/etc/pam.d/common-password") ||
|
|
&open_readfile(PAM, "/etc/pam.d/system-auth"))) {
|
|
# Then try reading common password config file
|
|
while(<PAM>) {
|
|
s/#.*$//g;
|
|
if (/^password.*md5/) { $md5 = 1; }
|
|
elsif (/^password.*blowfish/) { $md5 = 2; }
|
|
elsif (/^password.*sha512/) { $md5 = 3; }
|
|
elsif (/^password.*yescrypt/) { $md5 = 4; }
|
|
}
|
|
close(PAM);
|
|
}
|
|
if (&open_readfile(DEFS, "/etc/login.defs")) {
|
|
# The login.defs file is used on debian sometimes
|
|
while(<DEFS>) {
|
|
s/#.*$//g;
|
|
$md5 = 1 if (/MD5_CRYPT_ENAB\s+yes/i);
|
|
}
|
|
close(DEFS);
|
|
}
|
|
$use_md5_cache = $md5;
|
|
return $md5;
|
|
}
|
|
|
|
1;
|