#!/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 "; } 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); =~ /^#!(\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 = ; 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"); &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() { 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() { 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; }