Files
2026-07-13 13:12:17 +08:00

1291 lines
34 KiB
Perl
Executable File

#!/usr/local/bin/perl
# A very simple perl web server used by Webmin
package miniserv;
use FindBin;
use lib $FindBin::RealBin;
BEGIN {
require 'miniserv-lib.pl';
}
# Find and read config file
if ($ARGV[0] eq "--nofork") {
$nofork_argv = 1;
shift(@ARGV);
}
if (@ARGV != 1) {
die "Usage: miniserv.pl <config file>";
}
if ($ARGV[0] =~ /^([a-z]:)?\//i) {
$config_file = $ARGV[0];
}
else {
chop($pwd = `pwd`);
$config_file = "$pwd/$ARGV[0]";
}
%config = &read_config_file($config_file);
$ENV{'LIBROOT'} = $config{'root'};
if ($config{'perllib'}) {
push(@INC, split(/:/, $config{'perllib'}));
push(@INC, "$config{'root'}/vendor_perl");
$ENV{'PERLLIB'} .= ':'.$config{'perllib'};
$ENV{'PERLLIB'} .= ':'."$config{'root'}/vendor_perl";
}
@startup_msg = ( );
# Check if SSL is enabled and available
if ($config{'ssl'}) {
eval "use Net::SSLeay";
if (!$@) {
$use_ssl = 1;
# These functions only exist for SSLeay 1.0
eval "Net::SSLeay::SSLeay_add_ssl_algorithms()";
eval "Net::SSLeay::load_error_strings()";
if (defined(&Net::SSLeay::X509_STORE_CTX_get_current_cert) &&
defined(&Net::SSLeay::CTX_load_verify_locations) &&
(defined(&Net::SSLeay::CTX_set_verify) ||
defined(&Net::SSLeay::set_verify))) {
$client_certs = 1;
}
}
}
# Check if IPv6 is enabled and available
eval "use Socket6";
$socket6err = $@;
if ($config{'ipv6'}) {
if (!$socket6err) {
push(@startup_msg, "IPv6 support enabled");
$use_ipv6 = 1;
}
else {
push(@startup_msg, "IPv6 support cannot be enabled without ".
"the Socket6 perl module");
}
}
# Check if the syslog module is available to log hacking attempts
if ($config{'syslog'}) {
eval "use Sys::Syslog qw(:DEFAULT setlogsock)";
if (!$@) {
$use_syslog = 1;
}
}
# check if the TCP-wrappers module is available
if ($config{'libwrap'}) {
eval "use Authen::Libwrap qw(hosts_ctl STRING_UNKNOWN)";
if (!$@) {
$use_libwrap = 1;
}
}
# Check if the MD5 perl module is available
eval "use MD5; \$dummy = new MD5; \$dummy->add('foo');";
if (!$@) {
$use_md5 = "MD5";
}
else {
eval "use Digest::MD5; \$dummy = new Digest::MD5; \$dummy->add('foo');";
if (!$@) {
$use_md5 = "Digest::MD5";
}
}
if ($use_md5) {
push(@startup_msg, "Using MD5 module $use_md5");
}
# Check if the crypt function supports SHA512
if (&unix_crypt_supports_sha512()) {
$use_sha512 = 1;
push(@startup_msg, "Using SHA512 via crypt() function");
}
# Check if Digest::SHA with hmac_sha256_hex is available, for keying
# the session-ID lookup table.
eval "use Digest::SHA qw(hmac_sha256_hex); hmac_sha256_hex('x', 'y');";
if (!$@) {
$use_hmac_sha256 = 1;
push(@startup_msg, "Using HMAC-SHA256 for session ID hashing");
}
# Get miniserv's perl path and location
$miniserv_path = $0;
open(SOURCE, $miniserv_path);
<SOURCE> =~ /^#!(\S+)/;
$perl_path = $1;
close(SOURCE);
if (!-x $perl_path) {
$perl_path = $^X;
}
if (-l $perl_path) {
$linked_perl_path = readlink($perl_path);
}
# Check vital config options
&update_vital_config();
# Check if already running via the PID file. In foreground mode, systemd or
# the socket bind owns duplicate-start detection.
if (!$config{'nofork'} && !$nofork_argv && open(PIDFILE, $config{'pidfile'})) {
my $already = <PIDFILE>;
close(PIDFILE);
chomp($already);
$already =~ s/^\s+|\s+$//g;
if ($already =~ /^\d+$/ && $already != $$ && kill(0, $already)) {
die "Webmin is already running with PID $already\n";
}
}
$sidname = $config{'sidname'};
# check if the PAM module is available to authenticate
if ($config{'assume_pam'}) {
# Just assume that it will work. This can also be used to work around
# a Solaris bug in which using PAM before forking caused it to fail
# later!
$use_pam = 1;
}
elsif (!$config{'no_pam'}) {
eval "use Authen::PAM;";
if (!$@) {
# check if the PAM authentication can be used by opening a
# PAM handle
local $pamh;
if (ref($pamh = new Authen::PAM($config{'pam'},
$config{'pam_test_user'},
\&pam_conv_func))) {
# Now test a login to see if /etc/pam.d/webmin is set
# up properly.
$pam_conv_func_called = 0;
$pam_username = "test";
$pam_password = "test";
my $pam_ret = $pamh->pam_authenticate();
if ($pam_conv_func_called ||
$pam_ret == PAM_SUCCESS()) {
push(@startup_msg,
"PAM authentication enabled");
$use_pam = 1;
}
else {
push(@startup_msg,
"PAM test failed - maybe ".
"/etc/pam.d/$config{'pam'} does not exist");
}
}
else {
push(@startup_msg,
"PAM initialization of Authen::PAM failed");
}
}
}
if ($config{'pam_only'} && !$use_pam) {
foreach $msg (@startup_msg) {
&log_error($msg);
}
&log_error("PAM use is mandatory, but could not be enabled!");
&log_error("no_pam and pam_only both are set!") if ($config{no_pam});
exit(1);
}
elsif ($pam_msg && !$use_pam) {
push(@startup_msg,
"Continuing without the Authen::PAM perl module");
}
# Check if the User::Utmp perl module is installed
if ($config{'utmp'}) {
eval "use User::Utmp;";
if (!$@) {
$write_utmp = 1;
push(@startup_msg, "UTMP logging enabled");
}
else {
push(@startup_msg,
"Perl module User::Utmp needed for Utmp logging is ".
"not installed : $@");
}
}
# See if the crypt function fails
eval "crypt('foo', 'xx')";
if ($@) {
eval "use Crypt::UnixCrypt";
if (!$@) {
$use_perl_crypt = 1;
push(@startup_msg,
"Using Crypt::UnixCrypt for password encryption");
}
else {
push(@startup_msg,
"crypt() function un-implemented, and Crypt::UnixCrypt ".
"not installed - password authentication will fail");
}
}
# Check if /dev/urandom really generates random IDs, by calling it twice
local $rand1 = &generate_random_id(1);
local $rand2 = &generate_random_id(1);
if ($rand1 eq $rand2) {
$bad_urandom = 1;
push(@startup_msg,
"Random number generator file /dev/urandom is not reliable");
}
# Check if we can call sudo
if ($config{'sudo'} && &has_command("sudo")) {
$use_sudo = 1;
}
# Change dir to the server root
@roots = ( $config{'root'} );
for($i=0; defined($config{"extraroot_$i"}); $i++) {
push(@roots, $config{"extraroot_$i"});
}
chdir($roots[0]);
eval { $user_homedir = (getpwuid($<))[7]; };
if ($@) {
# getpwuid doesn't work on windows
$user_homedir = $ENV{"HOME"} || $ENV{"USERPROFILE"} || "/";
$on_windows = 1;
}
# Read users file
&read_users_file();
# Setup SSL if possible and if requested
if (!-r $config{'keyfile'}) {
# Key file doesn't exist!
if ($config{'keyfile'}) {
&log_error("SSL key file $config{'keyfile'} does not exist");
}
$use_ssl = 0;
}
elsif ($config{'certfile'} && !-r $config{'certfile'}) {
# Cert file doesn't exist!
&log_error("SSL cert file $config{'certfile'} does not exist");
$use_ssl = 0;
}
if ($use_ssl) {
$client_certs = 0 if (!-r $config{'ca'} || !%certs);
$err = &setup_ssl_contexts();
die $err if ($err);
}
# Load gzip library if enabled
if ($config{'gzip'} eq '1') {
eval "use Compress::Zlib";
if (!$@) {
$use_gzip = 1;
}
}
# Read websockets configs
&parse_websockets_config();
# Setup syslog support if possible and if requested
if ($use_syslog) {
open(ERRDUP, ">&STDERR");
open(STDERR, ">/dev/null");
$log_socket = $config{"logsock"} || "unix";
eval 'openlog($config{"pam"}, "cons,pid,ndelay", "authpriv"); setlogsock($log_socket)';
if ($@) {
$use_syslog = 0;
}
else {
local $msg = ucfirst($config{'pam'});
$msg .= $ENV{'STARTED'}++ ?
" reloaded configuration" : " starting";
eval { syslog("info", "%s", $msg); };
if ($@) {
eval {
setlogsock("inet");
syslog("info", "%s", $msg);
};
if ($@) {
# All attempts to use syslog have failed..
$use_syslog = 0;
}
}
}
open(STDERR, ">&ERRDUP");
close(ERRDUP);
}
# Read MIME types file and add extra types
&read_mime_types();
# get the time zone
if ($config{'log'}) {
local(@gmt, @lct, $days, $hours, $mins);
@gmt = gmtime(time());
@lct = localtime(time());
$days = $lct[3] - $gmt[3];
$hours = ($days < -1 ? 24 : 1 < $days ? -24 : $days * 24) +
$lct[2] - $gmt[2];
$mins = $hours * 60 + $lct[1] - $gmt[1];
$timezone = ($mins < 0 ? "-" : "+"); $mins = abs($mins);
$timezone .= sprintf "%2.2d%2.2d", $mins/60, $mins%60;
}
# Build various maps from the config files
&build_config_mappings();
# start up external authentication program, if needed
if ($config{'extauth'}) {
socketpair(EXTAUTH, EXTAUTH2, AF_UNIX, SOCK_STREAM, PF_UNSPEC);
if (!($extauth = fork())) {
close(EXTAUTH);
close(STDIN);
close(STDOUT);
open(STDIN, "<&EXTAUTH2");
open(STDOUT, ">&EXTAUTH2");
exec($config{'extauth'}) or die "exec failed : $!\n";
}
close(EXTAUTH2);
local $os = select(EXTAUTH);
$| = 1; select($os);
}
# Pre-load any libraries
foreach $pl (split(/\s+/, $config{'preload'})) {
($pkg, $lib) = split(/=/, $pl);
$pkg =~ s/[^A-Za-z0-9]/_/g;
eval "package $pkg; do '$config{'root'}/$lib'";
if ($@) {
&log_error("Failed to pre-load $lib in $pkg : $@");
}
}
foreach $pl (split(/\s+/, $config{'premodules'})) {
if ($pl =~ /\//) {
($dir, $mod) = split(/\//, $pl);
}
else {
($dir, $mod) = (undef, $pl);
}
push(@INC, "$config{'root'}/$dir");
eval "package $mod; use $mod ()";
if ($@) {
&log_error("Failed to pre-load $mod : $@");
}
}
foreach $mod (split(/\s+/, $config{'preuse'})) {
eval "use $mod;";
if ($@) {
&log_error("Failed to pre-load $mod : $@");
}
}
# Open debug log if set
&open_debug_to_log("miniserv.pl starting ..\n");
# Write out (empty) blocked hosts file
&write_blocked_file();
# Initially read webmin cron functions and last execution times
&read_webmin_crons();
%webmincron_last = ( );
&read_file($config{'webmincron_last'}, \%webmincron_last);
# Pre-cache lang files
&precache_files();
# Clear any flag files to prevent restart loops
unlink($config{'restartflag'}) if ($config{'restartflag'});
unlink($config{'reloadflag'}) if ($config{'reloadflag'});
unlink($config{'stopflag'}) if ($config{'stopflag'});
# Build list of sockets to listen on
@listening_on_ports = ();
$config{'bind'} = '' if ($config{'bind'} eq '*');
if ($config{'bind'}) {
# Listening on a specific IP
if (&check_ip6address($config{'bind'})) {
# IP is v6
$use_ipv6 || die "Cannot bind to $config{'bind'} without IPv6";
push(@sockets, [ inet_pton(AF_INET6(),$config{'bind'}),
$config{'port'},
PF_INET6() ]);
}
else {
# IP is v4
push(@sockets, [ inet_aton($config{'bind'}),
$config{'port'},
PF_INET() ]);
}
}
else {
# Listening on all IPs
push(@sockets, [ INADDR_ANY, $config{'port'}, PF_INET() ]);
if ($use_ipv6) {
# Also IPv6
push(@sockets, [ in6addr_any(), $config{'port'},
PF_INET6() ]);
}
}
foreach $s (split(/\s+/, $config{'sockets'})) {
if ($s =~ /^(\d+)$/) {
# Just listen on another port on the main IP
push(@sockets, [ $sockets[0]->[0], $s, $sockets[0]->[2] ]);
if ($use_ipv6 && !$config{'bind'}) {
# Also listen on that port on the main IPv6 address
push(@sockets, [ $sockets[1]->[0], $s,
$sockets[1]->[2] ]);
}
}
elsif ($s =~ /^\*:(\d+)$/) {
# Listening on all IPs on some port
push(@sockets, [ INADDR_ANY, $1,
PF_INET() ]);
if ($use_ipv6) {
push(@sockets, [ in6addr_any(), $1,
PF_INET6() ]);
}
}
elsif ($s =~ /^(\S+):(\d+)$/) {
# Listen on a specific port and IP
my ($ip, $port) = ($1, $2);
if (&check_ip6address($ip)) {
$use_ipv6 || die "Cannot bind to $ip without IPv6";
push(@sockets, [ inet_pton(AF_INET6(),
$ip),
$port, PF_INET6() ]);
}
else {
push(@sockets, [ inet_aton($ip), $port,
PF_INET() ]);
}
}
elsif ($s =~ /^([0-9\.]+):\*$/ || $s =~ /^([0-9\.]+)$/) {
# Listen on the main port on another IPv4 address
push(@sockets, [ inet_aton($1), $sockets[0]->[1],
PF_INET() ]);
}
elsif (($s =~ /^([0-9a-f\:]+):\*$/ || $s =~ /^([0-9a-f\:]+)$/) &&
$use_ipv6) {
# Listen on the main port on another IPv6 address
push(@sockets, [ inet_pton(AF_INET6(), $1),
$sockets[0]->[1],
PF_INET6() ]);
}
}
# Open all the sockets
$proto = getprotobyname('tcp');
@sockerrs = ( );
$tried_inaddr_any = 0;
for($i=0; $i<@sockets; $i++) {
$fh = "MAIN$i";
if (!socket($fh, $sockets[$i]->[2], SOCK_STREAM, $proto)) {
# Protocol not supported
push(@sockerrs, "Failed to open socket family $sockets[$i]->[2] : $!");
next;
}
setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
if ($sockets[$i]->[2] eq PF_INET()) {
$pack = pack_sockaddr_in($sockets[$i]->[1], $sockets[$i]->[0]);
}
else {
$pack = pack_sockaddr_in6($sockets[$i]->[1], $sockets[$i]->[0]);
setsockopt($fh, 41, 26, pack("l", 1)); # IPv6 only
}
for($j=0; $j<5; $j++) {
last if (bind($fh, $pack));
sleep(1);
}
if ($j == 5) {
# All attempts failed .. give up
if ($sockets[$i]->[0] eq INADDR_ANY ||
$use_ipv6 && $sockets[$i]->[0] eq in6addr_any()) {
push(@sockerrs,
"Failed to bind to port $sockets[$i]->[1] : $!");
$tried_inaddr_any = 1;
}
else {
$ip = &network_to_address($sockets[$i]->[0]);
push(@sockerrs,
"Failed to bind to IP $ip port ".
"$sockets[$i]->[1] : $!");
}
}
else {
listen($fh, &get_somaxconn());
push(@socketfhs, $fh);
push(@listening_on_ports, $sockets[$i]->[1]);
$ipv6fhs{$fh} = $sockets[$i]->[2] eq PF_INET() ? 0 : 1;
}
}
foreach $se (@sockerrs) {
&log_error($se);
}
# If all binds failed, try binding to any address
if (!@socketfhs && !$tried_inaddr_any) {
&log_error("Falling back to listening on any address");
$fh = "MAIN";
socket($fh, PF_INET(), SOCK_STREAM, $proto) ||
die "Failed to open socket : $!";
setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
if (!bind($fh, pack_sockaddr_in($sockets[0]->[1], INADDR_ANY))) {
&log_error("Failed to bind to port $sockets[0]->[1] : $!");
exit(1);
}
listen($fh, &get_somaxconn());
push(@socketfhs, $fh);
}
elsif (!@socketfhs && $tried_inaddr_any) {
&log_error("Could not listen on any ports");
exit(1);
}
if ($config{'listen'}) {
# Open the socket that allows other webmin servers to find this one
$proto = getprotobyname('udp');
if (socket(LISTEN, PF_INET(), SOCK_DGRAM, $proto)) {
setsockopt(LISTEN, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
bind(LISTEN, pack_sockaddr_in($config{'listen'}, INADDR_ANY));
listen(LISTEN, &get_somaxconn());
}
else {
$config{'listen'} = 0;
}
}
# Split from the controlling terminal, unless configured not to
if (!$config{'nofork'} && !$nofork_argv) {
if (fork()) { exit; }
}
eval { setsid(); }; # may not work on Windows
# Close standard file handles
open(STDIN, "</dev/null");
open(STDOUT, ">/dev/null");
&redirect_stderr_to_log();
&log_error("miniserv.pl started");
foreach $msg (@startup_msg) {
&log_error($msg);
}
# write out the PID file
&write_pid_file();
$miniserv_main_pid = $$;
# Start the log-clearing process, if needed. This checks every minute
# to see if the log has passed its reset time, and if so clears it
if ($config{'logclear'}) {
if (!($logclearer = fork())) {
&close_all_sockets();
close(LISTEN);
while(1) {
local $write_logtime = 0;
local @st = stat("$config{'logfile'}.time");
if (@st) {
if ($st[9]+$config{'logtime'}*60*60 < time()){
# need to clear log
$write_logtime = 1;
unlink($config{'logfile'});
unlink($config{'errorlog'})
if ($config{'errorlog'} &&
$config{'errorlog'} ne '-');
unlink($config{'debuglog'})
if ($config{'debuglog'});
}
}
else {
$write_logtime = 1;
}
if ($write_logtime) {
open(LOGTIME, ">$config{'logfile'}.time");
print LOGTIME time(),"\n";
close(LOGTIME);
}
sleep(5*60);
}
exit;
}
push(@childpids, $logclearer);
}
# Setup the logout time dbm if needed
if ($config{'session'}) {
&open_session_db();
}
# Run the main loop
$SIG{'HUP'} = 'miniserv::trigger_restart';
$SIG{'TERM'} = 'miniserv::term_handler';
$SIG{'USR1'} = 'miniserv::trigger_reload';
$SIG{'PIPE'} = 'IGNORE';
local $remove_session_count = 0;
$need_pipes = $config{'passdelay'} || $config{'session'};
$cron_runs = 0;
while(1) {
# Periodically re-open error and debug logs if deleted via regular
# log clearing
if ($config{'errorlog'} && $config{'errorlog'} ne '-' &&
!-e $config{'errorlog'}) {
&redirect_stderr_to_log();
}
if ($config{'debuglog'} && !-e $config{'debuglog'}) {
&open_debug_to_log();
}
# Check if any webmin cron jobs are ready to run
&execute_ready_webmin_crons($cron_runs++);
# wait for a new connection, or a message from a child process
local ($i, $rmask);
if (@childpids <= $config{'maxconns'}) {
# Only accept new main socket connects when ready
local $s;
foreach $s (@socketfhs) {
vec($rmask, fileno($s), 1) = 1;
}
}
else {
printf STDERR "too many children (%d > %d)\n",
scalar(@childpids), $config{'maxconns'};
}
if ($need_pipes) {
for($i=0; $i<@passin; $i++) {
vec($rmask, fileno($passin[$i]), 1) = 1;
}
}
vec($rmask, fileno(LISTEN), 1) = 1 if ($config{'listen'});
# Wait for a connection
local $sel = select($rmask, undef, undef, 2);
# Check the flag files
if ($config{'restartflag'} && -r $config{'restartflag'}) {
unlink($config{'restartflag'});
$need_restart = 1;
}
if ($config{'reloadflag'} && -r $config{'reloadflag'}) {
unlink($config{'reloadflag'});
$need_reload = 1;
}
if ($config{'stopflag'} && -r $config{'stopflag'}) {
unlink($config{'stopflag'});
$need_stop = 1;
}
if ($need_restart) {
# Got a HUP signal while in select() .. restart now
&restart_miniserv();
}
if ($need_reload) {
# Got a USR1 signal while in select() .. re-read config
$need_reload = 0;
&reload_config_file();
}
if ($need_stop) {
# Stop flag file created
&term_handler();
}
local $time_now = time();
# Clean up processes that have been idle for too long, if configured
if ($config{'maxlifetime'}) {
foreach my $c (@childpids) {
my $age = time() - $childstarts{$c};
if ($childstarts{$c} &&
$age > $config{'maxlifetime'}) {
kill(9, $c);
&log_error("Killing long-running process $c after $age seconds");
delete($childstarts{$c});
}
}
}
# Clean up finished processes
local $pid;
do { $pid = waitpid(-1, WNOHANG);
@childpids = grep { $_ != $pid } @childpids;
} while($pid != 0 && $pid != -1);
@childpids = grep { kill(0, $_) } @childpids;
my %childpids = map { $_, 1 } @childpids;
foreach my $s (keys %childstarts) {
delete($childstarts{$s}) if (!$childpids{$s});
}
# Clean up connection counts from IPs that are no longer in use
foreach my $ip (keys %ipconnmap) {
$ipconnmap{$ip} = [ grep { $childpids{$_} } @{$ipconnmap{$ip}}];
}
foreach my $net (keys %netconnmap) {
$netconnmap{$net} = [ grep { $childpids{$_} } @{$netconnmap{$net}}];
}
# run the unblocking procedure to check if enough time has passed to
# unblock hosts that never been blocked because of password failures
$unblocked = 0;
if ($config{'blockhost_failures'}) {
$i = 0;
while ($i <= $#deny) {
if ($blockhosttime{$deny[$i]} &&
$config{'blockhost_time'} != 0 &&
($time_now - $blockhosttime{$deny[$i]}) >=
$config{'blockhost_time'}) {
# the host can be unblocked now
$hostfail{$deny[$i]} = 0;
splice(@deny, $i, 1);
$unblocked = 1;
}
$i++;
}
}
# Do the same for blocked users
if ($config{'blockuser_failures'}) {
$i = 0;
while ($i <= $#deny) {
if ($blockusertime{$deny[$i]} &&
$config{'blockuser_time'} != 0 &&
($time_now - $blockusertime{$deny[$i]}) >=
$config{'blockuser_time'}) {
# the user can be unblocked now
$userfail{$deny[$i]} = 0;
splice(@denyusers, $i, 1);
$unblocked = 1;
}
$i++;
}
}
if ($unblocked) {
&write_blocked_file();
}
if ($config{'session'} && (++$remove_session_count%50) == 0) {
# Remove sessions with more than 7 days of inactivity,
local $s;
foreach $s (keys %sessiondb) {
local ($user, $ltime, $lip) =
split(/\s+/, $sessiondb{$s});
if ($ltime && $time_now - $ltime > 7*24*60*60) {
&run_logout_script($s, $user, undef, undef);
&write_logout_utmp($user, $lip);
if ($user =~ /^\!/ || $sessiondb{$s} eq '') {
# Don't log anything for logged out
# sessions or those with no data
}
elsif ($use_syslog && $user) {
syslog("info", "%s",
"Timeout of session for $user");
}
elsif ($use_syslog) {
syslog("info", "%s",
"Timeout of unknown session $s ".
"with value $sessiondb{$s}");
}
delete($sessiondb{$s});
}
}
}
if ($use_pam && $config{'pam_conv'}) {
# Remove PAM sessions with more than 5 minutes of inactivity
local $c;
foreach $c (values %conversations) {
if ($time_now - $c->{'time'} > 5*60) {
&end_pam_conversation($c);
if ($use_syslog) {
syslog("info", "%s", "Timeout of PAM ".
"session for $c->{'user'}");
}
}
}
}
# Don't check any sockets if there is no activity
next if ($sel <= 0);
# Check if any of the main sockets have received a new connection
local $sn = 0;
foreach $s (@socketfhs) {
if (vec($rmask, fileno($s), 1)) {
# got new connection
$acptaddr = accept(SOCK, $s);
print DEBUG "accept returned ",length($acptaddr),"\n";
next if (!$acptaddr);
binmode(SOCK);
# Work out IP and port of client
local ($peerb, $peera, $peerp) =
&get_address_ip($acptaddr, $ipv6fhs{$s});
print DEBUG "peera=$peera peerp=$peerp\n";
# Check the number of connections from this IP
$ipconnmap{$peera} ||= [ ];
$ipconns = $ipconnmap{$peera};
if ($config{'maxconns_per_ip'} >= 0 &&
@$ipconns > $config{'maxconns_per_ip'}) {
&log_error("Too many connections (",scalar(@$ipconns),") from IP $peera");
close(SOCK);
next;
}
# Also check the number of connections from the network
($peernet = $peera) =~ s/\.\d+$/\.0/;
$netconnmap{$peernet} ||= [ ];
$netconns = $netconnmap{$peernet};
if ($config{'maxconns_per_net'} >= 0 &&
@$netconns > $config{'maxconns_per_net'}) {
&log_error("Too many connections (",scalar(@$netconns),") from network $peernet");
close(SOCK);
next;
}
# create pipes
local ($PASSINr, $PASSINw, $PASSOUTr, $PASSOUTw);
if ($need_pipes) {
($PASSINr, $PASSINw, $PASSOUTr, $PASSOUTw) =
&allocate_pipes();
}
# Work out the local IP
(undef, $locala) = &get_socket_ip(SOCK, $ipv6fhs{$s});
print DEBUG "locala=$locala\n";
# Check username of connecting user
$localauth_user = undef;
if ($config{'localauth'} && $peera eq "127.0.0.1") {
if (open(TCP, "/proc/net/tcp")) {
# Get the info direct from the kernel
$peerh = sprintf("%4.4X", $peerp);
while(<TCP>) {
s/^\s+//;
local @t = split(/[\s:]+/, $_);
if ($t[1] eq '0100007F' &&
$t[2] eq $peerh) {
$localauth_user =
getpwuid($t[11]);
last;
}
}
close(TCP);
}
if (!$localauth_user) {
# Call lsof for the info
local $lsofpid = open(LSOF,
"$config{'localauth'} -i ".
"TCP\@127.0.0.1:$peerp |");
while(<LSOF>) {
if (/^(\S+)\s+(\d+)\s+(\S+)/ &&
$2 != $$ && $2 != $lsofpid){
$localauth_user = $3;
}
}
close(LSOF);
}
}
# Work out the hostname for this web server
$host = &get_socket_name(SOCK, $ipv6fhs{$s});
if (!$host) {
&log_error(
"Failed to get local socket name : $!");
close(SOCK);
next;
}
$port = $sockets[$sn]->[1];
# fork the subprocess
local $handpid;
if (!($handpid = fork())) {
# setup signal handlers
print DEBUG "in subprocess\n";
$SIG{'TERM'} = 'DEFAULT';
$SIG{'PIPE'} = 'DEFAULT';
#$SIG{'CHLD'} = 'IGNORE';
$SIG{'HUP'} = 'IGNORE';
$SIG{'USR1'} = 'IGNORE';
# Close the file handle for the session DBM
dbmclose(%sessiondb);
# close useless pipes
if ($need_pipes) {
&close_all_pipes();
close($PASSINr); close($PASSOUTw);
}
&close_all_sockets();
close(LISTEN);
# Initialize SSL for this connection
if ($use_ssl) {
my $byte = '';
# Look at the first byte of the socket
# buffer but don't consume it
recv(SOCK, $byte, 1, MSG_PEEK);
if (length($byte) &&
# Check if the first byte is a TLS
(ord($byte) == 0x16 ||
# Check if the first byte is SSL
(ord($byte) & 0x80))) {
($ssl_con,
$ssl_certfile,
$ssl_keyfile,
$ssl_cn,
$ssl_alts) =
&ssl_connection_for_ip(
SOCK, $ipv6fhs{$s});
print DEBUG "ssl_con returned ".
"$ssl_con\n";
$ssl_con || exit;
}
else {
$use_ssl = 0;
}
}
print DEBUG
"main: Starting handle_request loop pid=$$\n";
while(&handle_request($peera, $locala,
$ipv6fhs{$s})) {
# Loop until keepalive stops
}
print DEBUG
"main: Done handle_request loop pid=$$\n";
if ($use_ssl) {
Net::SSLeay::shutdown($ssl_con);
}
shutdown(SOCK, 1);
close(SOCK);
close($PASSINw); close($PASSOUTw);
exit;
}
push(@childpids, $handpid);
$childstarts{$handpid} = time();
push(@$ipconns, $handpid);
push(@$netconns, $handpid);
if ($need_pipes) {
close($PASSINw); close($PASSOUTr);
push(@passin, $PASSINr);
push(@passout, $PASSOUTw);
}
close(SOCK);
}
$sn++;
}
if ($config{'listen'} && vec($rmask, fileno(LISTEN), 1)) {
# Got UDP packet from another webmin server
local $rcvbuf;
local $from = recv(LISTEN, $rcvbuf, 1024, 0);
next if (!$from);
local $fromip = inet_ntoa((unpack_sockaddr_in($from))[1]);
local $toip = inet_ntoa((unpack_sockaddr_in(
getsockname(LISTEN)))[1]);
# Check for any rate limits
my $ratelimit = 0;
if ($last_udp{$fromip} &&
time() - $last_udp{$fromip} < $config{'listen_delay'}) {
$ratelimit = 1;
}
else {
$last_udp{$fromip} = time();
}
if (!$ratelimit &&
(!@deny || !&ip_match($fromip, $toip, @deny)) &&
(!@allow || &ip_match($fromip, $toip, @allow))) {
local $listenhost = &get_socket_name(LISTEN, 0);
send(LISTEN, "$listenhost:$config{'port'}:".
($use_ssl ? 1 : 0).":".
($config{'listenhost'} ?
&get_system_hostname() : ""),
0, $from)
if ($listenhost);
}
}
# check for session, password-timeout and PAM messages from subprocesses
for($i=0; $i<@passin; $i++) {
if (vec($rmask, fileno($passin[$i]), 1)) {
# this sub-process is asking about a password
local $infd = $passin[$i];
local $outfd = $passout[$i];
local $inline = &sysread_line($infd);
if ($inline) {
print DEBUG "main: inline $inline";
}
else {
print DEBUG "main: inline EOF\n";
}
# Search for two-factor authentication flag
# being passed, to mark the call as safe
$inline =~ /^delay\s+(\S+)\s+(\S+)\s+(\d+)\s+(nolog)/;
local $nolog = $4;
if ($inline =~ /^delay\s+(\S+)\s+(\S+)\s+(\d+)/) {
# Got a delay request from a subprocess.. for
# valid logins, there is no delay (to prevent
# denial of service attacks), but for invalid
# logins the delay increases with each failed
# attempt.
if ($3) {
# login OK.. no delay
print $outfd "0 0\n";
$wasblocked = $hostfail{$2} ||
$userfail{$1};
$hostfail{$2} = 0;
$userfail{$1} = 0;
if ($wasblocked) {
&write_blocked_file();
}
}
else {
# Login failed..
$hostfail{$2}++ if (!$nolog);
$userfail{$1}++ if (!$nolog && $1 ne "-");
$blocked = 0;
# Add the host to the block list,
# if configured
if ($config{'blockhost_failures'} &&
$hostfail{$2} >=
$config{'blockhost_failures'}) {
push(@deny, $2);
$blockhosttime{$2} = $time_now;
$blocked = 1;
if ($use_syslog) {
local $logtext = "Security alert: Host $2 blocked after $config{'blockhost_failures'} failed logins for user $1";
syslog("crit", "%s",
$logtext);
}
}
# Add the user to the user block list,
# if configured
if ($1 ne "-" &&
$config{'blockuser_failures'} &&
$userfail{$1} >=
$config{'blockuser_failures'}) {
push(@denyusers, $1);
$blockusertime{$1} = $time_now;
$blocked = 2;
if ($use_syslog) {
local $logtext = "Security alert: User $1 blocked after $config{'blockuser_failures'} failed logins";
syslog("crit", "%s",
$logtext);
}
}
# Lock out the user's password, if enabled
if ($1 ne "-" &&
$config{'blocklock'} &&
$userfail{$1} >=
$config{'blockuser_failures'}) {
my $lk = &lock_user_password($1);
$blocked = 2;
if ($use_syslog) {
local $logtext = $lk == 1 ? "Security alert: User $1 locked after $config{'blockuser_failures'} failed logins" : $lk < 0 ? "Security alert: User could not be locked" : "Security alert: User is already locked";
syslog("crit", "%s",
$logtext);
}
}
# Send back a delay
$dl = $userdlay{$1} -
int(($time_now - $userlast{$1})/50);
$dl = $dl < 0 ? 0 : $dl+1;
print $outfd "$dl $blocked\n";
$userdlay{$1} = $dl;
# Write out blocked status file
if ($blocked) {
&write_blocked_file();
}
}
$userlast{$1} = $time_now;
}
elsif ($inline =~ /^verify\s+(\S+)\s+(\S+)\s+(\S+)/) {
# Verifying a session ID
local $session_id = $1;
local $vip = $2;
local $uptime = $3;
local $skey = $sessiondb{$session_id} ?
$session_id :
&hash_session_id($session_id);
if (!defined($sessiondb{$skey})) {
# Session doesn't exist
print $outfd "0 0\n";
}
else {
local ($user, $ltime, $ip, $lifetime) =
split(/\s+/, $sessiondb{$skey});
local $lot = &get_logout_time($user, $session_id);
if ($lot &&
$time_now - $ltime > $lot*60) {
# Session has timed out due to
# idle time being hit
print $outfd "1 ",($time_now - $ltime),"\n";
#delete($sessiondb{$skey});
}
elsif ($lifetime && $time_now - $ltime > $lifetime) {
# Session has timed out due to
# lifetime exceeded
print $outfd "1 ",($time_now - $ltime),"\n";
}
elsif ($ip && $vip && $ip ne $vip &&
$config{'session_ip'}) {
# Session was OK, but from the
# wrong IP address
print $outfd "3 $ip\n";
}
elsif ($user =~ /^\!/) {
# Logged out session
print $outfd "0 0\n";
}
else {
# Session is OK, update last time
# and remote IP
print $outfd "2 $user\n";
if ($uptime) {
$sessiondb{$skey} = "$user $time_now $vip";
}
}
}
}
elsif ($inline =~ /^new\s+(\S+)\s+(\S+)\s+(\S+)/) {
# Creating a new session
local $session_id = $1;
local $user = $2;
local $ip = $3;
$sessiondb{&hash_session_id($session_id)} =
"$user $time_now $ip";
}
elsif ($inline =~ /^delete\s+(\S+)/) {
# Logging out a session
local $session_id = $1;
local $skey = $sessiondb{$session_id} ?
$session_id :
&hash_session_id($session_id);
local ($user, $ltime, $ip) =
split(/\s+/, $sessiondb{$skey});
$user =~ s/^\!//;
print $outfd $user,"\n";
$sessiondb{$skey} = "!$user $ltime $ip";
}
elsif ($inline =~ /^pamstart\s+(\S+)\s+(\S+)\s+(.*)/) {
# Starting a new PAM conversation
local ($cid, $host, $user) = ($1, $2, $3);
# Does this user even need PAM?
local ($realuser, $canlogin) =
&can_user_login($user, undef, $host);
local $conv;
if ($canlogin == 0) {
# Cannot even login!
print $outfd "0 Invalid username\n";
}
elsif ($canlogin != 2) {
# Not using PAM .. so just ask for
# the password.
$conv = { 'user' => $realuser,
'host' => $host,
'step' => 0,
'cid' => $cid,
'time' => time() };
print $outfd "3 Password\n";
}
else {
# Start the PAM conversation
# sub-process, and get a question
$conv = { 'user' => $realuser,
'host' => $host,
'cid' => $cid,
'time' => time() };
local ($PAMINr, $PAMINw, $PAMOUTr,
$PAMOUTw) = &allocate_pipes();
local $pampid = fork();
if (!$pampid) {
close($PAMOUTr); close($PAMINw);
&pam_conversation_process(
$realuser,
$PAMOUTw, $PAMINr);
}
close($PAMOUTw); close($PAMINr);
$conv->{'pid'} = $pampid;
$conv->{'PAMOUTr'} = $PAMOUTr;
$conv->{'PAMINw'} = $PAMINw;
push(@childpids, $pampid);
# Get the first PAM question
local $pok = &recv_pam_question(
$conv, $outfd);
if (!$pok) {
&end_pam_conversation($conv);
}
}
$conversations{$cid} = $conv if ($conv);
}
elsif ($inline =~ /^pamanswer\s+(\S+)\s+(.*)/) {
# A response to a PAM question
local ($cid, $answer) = ($1, $2);
local $conv = $conversations{$cid};
if (!$conv) {
# No such conversation?
print $outfd "0 Bad login session\n";
}
elsif ($conv->{'pid'}) {
# Send the PAM response and get
# the next question
&send_pam_answer($conv, $answer);
local $pok = &recv_pam_question($conv, $outfd);
if (!$pok) {
&end_pam_conversation($conv);
}
}
else {
# This must be the password .. try it
# and send back the results
local ($vu, $expired, $nonexist) =
&validate_user_caseless(
$conv->{'user'},
$answer,
$conf->{'host'});
local $ok = $vu ? 1 : 0;
print $outfd "2 $conv->{'user'} $ok $expired $notexist\n";
&end_pam_conversation($conv);
}
}
elsif ($inline =~ /^writesudo\s+(\S+)\s+(\d+)/) {
# Store the fact that some user can sudo to root
local ($user, $ok) = ($1, $2);
$sudocache{$user} = $ok." ".time();
}
elsif ($inline =~ /^readsudo\s+(\S+)/) {
# Query the user sudo cache (valid for 1 minute)
local $user = $1;
local ($ok, $last) =
split(/\s+/, $sudocache{$user});
if ($last < time()-60) {
# Cache too old
print $outfd "2\n";
}
else {
# Tell client OK or not
print $outfd "$ok\n";
}
}
elsif ($inline =~ /\S/) {
# Unknown line from pipe?
print DEBUG "main: Unknown line from pipe $inline\n";
&log_error("Unknown line from pipe $inline");
}
else {
# close pipe
close($infd); close($outfd);
$passin[$i] = $passout[$i] = undef;
}
}
}
@passin = grep { defined($_) } @passin;
@passout = grep { defined($_) } @passout;
}