#!/usr/bin/perl
# A very simple perl web server used by Webmin
# Require basic libraries
package miniserv;
use Socket;
use POSIX;
use Time::Local;
eval "use Time::HiRes;";
@itoa64 = split(//, "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
@miniserv_argv = @ARGV;
# 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 SHA512 perl module is available
eval "use Crypt::SHA";
$use_sha512 = $@ ? "Crypt::SHA" : undef;
if ($use_sha512) {
push(@startup_msg, "Using SHA512 module $use_sha512");
}
# 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
if (open(PIDFILE, $config{'pidfile'})) {
my $already = ;
close(PIDFILE);
chop($already);
if ($already && $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;
}
# init days and months for http_date
@weekday = ( "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" );
@month = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" );
# 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'}) {
eval "use SDBM_File";
dbmopen(%sessiondb, $config{'sessiondb'}, 0700);
eval "\$sessiondb{'1111111111'} = 'foo bar';";
if ($@) {
dbmclose(%sessiondb);
eval "use NDBM_File";
dbmopen(%sessiondb, $config{'sessiondb'}, 0700);
}
else {
delete($sessiondb{'1111111111'});
}
}
# 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;
}
# handle_request(remoteaddress, localaddress, ipv6-flag)
# Where the real work is done
sub handle_request
{
local ($acptip, $localip, $ipv6) = @_;
seek(DEBUG, 0, 2);
print DEBUG "handle_request: from $acptip to $localip ipv6=$ipv6\n";
if ($config{'loghost'}) {
$acpthost = &to_hostname($acptip);
$acpthost = $acptip if (!$acpthost);
}
else {
$acpthost = $acptip;
}
$loghost = $acpthost;
$datestr = &http_date(time());
$ok_code = 200;
$ok_message = "Document follows";
$logged_code = undef;
$reqline = $request_uri = $page = undef;
$authuser = undef;
$validated = undef;
# check address against access list
if (@deny && &ip_match($acptip, $localip, @deny) ||
@allow && !&ip_match($acptip, $localip, @allow)) {
&http_error(403, "Access denied for ".&html_strip($acptip));
return 0;
}
if ($use_libwrap) {
# Check address with TCP-wrappers
if (!hosts_ctl($config{'pam'}, STRING_UNKNOWN,
$acptip, STRING_UNKNOWN)) {
&http_error(403, "Access denied for ".&html_strip($acptip).
" by TCP wrappers");
return 0;
}
}
print DEBUG "handle_request: passed IP checks\n";
# Compute a timeout for the start of headers, based on the number of
# child processes. As this increases, we use a shorter timeout to avoid
# an attacker overloading the system.
local $header_timeout = 60 + ($config{'maxconns'} - @childpids) * 10;
if ($header_timeout > 10*60) {
$header_timeout = 10*60;
}
local $rmask;
vec($rmask, fileno(SOCK), 1) = 1;
local $to = $checked_timeout ? 10*60 : $header_timeout;
print DEBUG "handle_request: waiting for $to seconds\n";
local $sel = select($rmask, undef, undef, $to);
if (!$sel) {
if ($checked_timeout) {
print DEBUG "handle_request: exiting due to timeout of $to\n";
exit;
}
else {
&http_error(400, "Timeout",
"Waited for $to seconds for start of headers");
}
}
$checked_timeout++;
print DEBUG "handle_request: passed timeout check\n";
# Read the HTTP request line
alarm(10);
$SIG{'ALRM'} = sub { die "timeout" };
local $origreqline = &read_line();
($reqline = $origreqline) =~ s/\r|\n//g;
$method = $page = $request_uri = undef;
print DEBUG "handle_request reqline=$reqline\n";
alarm(0);
if (!$use_ssl && $config{'ssl'} && $config{'ssl_enforce'}) {
# This is an HTTP request when HTTPS should be enforced
my $musthost = $config{'musthost'};
my $hostheader;
if (!$musthost) {
# Read host HTTP header because we want one earlier
alarm(10);
local $SIG{'ALRM'} = sub { die "timeout" };
while (defined(my $line = &read_line())) {
$line =~ s/\r|\n//g;
last if $line eq '';
if ($line =~ /^host:\s*(.*)$/i) {
$hostheader = "https://$1";
last;
}
}
alarm(0);
}
# Host header must already contain full URL
my $url = $hostheader;
if (!$url) {
# No host header
local $urlhost = $musthost || $host;
$urlhost = "[".$urlhost."]" if (&check_ip6address($urlhost));
local $wantport = $port;
if ($wantport == 80 &&
&indexof(443, @listening_on_ports) >= 0) {
# Connection was to port 80, but since we are also
# accepting on port 443, redirect to that
$wantport = 443;
}
$url = $wantport == 443
? "https://$urlhost/"
: "https://$urlhost:$wantport/";
}
# Enforce HTTPS
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_data("Location: $url\r\n");
&write_keep_alive(0);
&write_data("\r\n");
&log_error("Redirecting HTTP request to $url");
&log_request($loghost, $authuser, $reqline, 302, 0);
return 0;
}
elsif (!$reqline && $checked_timeout > 1) {
# An empty request .. just close the connection
print DEBUG "handle_request: rejecting empty request\n";
return 0;
}
elsif ($reqline && $reqline !~ /^(\S+)\s+(.*)\s+HTTP\/1\..$/) {
&http_error(400, "Bad Request");
return 0;
}
$method = $1;
$request_uri = $page = $2;
%header = ();
# Read HTTP headers
alarm(60);
$SIG{'ALRM'} = sub { die "timeout" };
local $lastheader;
while(1) {
($headline = &read_line()) =~ s/\r|\n//g;
last if ($headline eq "");
print DEBUG "handle_request: got headline $headline\n";
if ($headline =~ /^(\S+):\s*(.*)$/) {
$header{$lastheader = lc($1)} = $2;
}
elsif ($headline =~ /^\s+(.*)$/) {
$header{$lastheader} .= $headline;
}
else {
alarm(0);
&http_error(400, "Bad Header ".&html_strip($headline));
}
if (&is_bad_header($header{$lastheader}, $lastheader)) {
alarm(0);
delete($header{$lastheader});
&http_error(400, "Bad Header Contents ".
&html_strip($lastheader));
}
}
alarm(0);
# If a remote IP is given in a header (such as via a proxy), only use it
# for logging unless trust_real_ip is set
local $headerhost = $header{'x-forwarded-for'} ||
$header{'x-real-ip'} ||
$header{'true-client-ip'} ||
$header{'cf-connecting-ip'} ||
$header{'cf-connecting-ip6'};
if ($headerhost) {
# Only real IPs are allowed
$headerhost = undef if (!&check_ipaddress($headerhost) &&
!&check_ip6address($headerhost));
}
if ($config{'trust_real_ip'}) {
$acpthost = $headerhost || $acpthost;
if (&check_ipaddress($headerhost) || &check_ip6address($headerhost)) {
# If a remote IP was given, use it for all access control checks
# from now on.
$acptip = $headerhost;
# re-check remote address against access list
if (@deny && &ip_match($acptip, $localip, @deny) ||
@allow && !&ip_match($acptip, $localip, @allow)) {
&http_error(403, "Access denied for ".&html_strip($acptip));
return 0;
}
if ($use_libwrap) {
# Check address with TCP-wrappers
if (!hosts_ctl($config{'pam'}, STRING_UNKNOWN,
$acptip, STRING_UNKNOWN)) {
&http_error(403, "Access denied for ".&html_strip($acptip).
" by TCP wrappers");
return 0;
}
}
print DEBUG "handle_request: passed Remote IP checks\n";
}
$loghost = $acpthost;
}
elsif ($config{'logtrust'}) {
# If a client IP address was provided, such as via a proxy, log it
$loghost = $headerhost || $loghost;
}
if (defined($header{'host'})) {
if ($header{'host'} =~ /^\[(.+)\]:([0-9]+)$/) {
($host, $port) = ($1, $2);
}
elsif ($header{'host'} =~ /^([^:]+):([0-9]+)$/) {
($host, $port) = ($1, $2);
}
else {
$host = $header{'host'};
}
if ($config{'musthost'} && $host ne $config{'musthost'} &&
!$config{'musthost_redirect'}) {
# Disallowed hostname used
&http_error(400, "Invalid HTTP hostname");
}
}
# Create strings for use in redirects
$ssl = $config{'redirect_ssl'} ne '' ? $config{'redirect_ssl'} : $use_ssl;
$redirport = $config{'redirect_port'} || $port;
$redirport = $config{'redirect_port'}
if ($config{'redirect_host'});
$portstr = $redirport == 80 && !$ssl ? "" :
$redirport == 443 && $ssl ? "" : ":".$redirport;
$redirhost = $config{'redirect_host'} || $host;
$hostport = &check_ip6address($redirhost) ? "[".$redirhost."]".$portstr
: $redirhost.$portstr;
# If the redirect_prefix exists change redirect base to include the prefix #1271
if ($config{'redirect_prefix'}) {
$hostport .= $config{'redirect_prefix'}
}
$prot = $ssl ? "https" : "http";
# Redirect to the configured "musthost", if "musthost_redirect" is set, rather
# than showing an error
if ($config{'musthost'} && $host ne $config{'musthost'} &&
$config{'musthost_redirect'}) {
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_data("Location: $prot://$config{'musthost'}:$redirport\r\n");
&write_keep_alive(0);
&write_data("\r\n");
&log_request($loghost, $authuser, $reqline, 302, 0) if $reqline;
shutdown(SOCK, 1);
close(SOCK);
return;
}
undef(%in);
if ($page =~ /^([^\?]+)\?(.*)$/) {
# There is some query string information
$page = $1;
$querystring = $2;
print DEBUG "handle_request: querystring=$querystring\n";
if ($querystring !~ /=/) {
$queryargs = $querystring;
$queryargs =~ s/\+/ /g;
$queryargs =~ s/%(..)/pack("c",hex($1))/ge;
$querystring = "";
}
else {
# Parse query-string parameters
local @in = split(/\&/, $querystring);
foreach $i (@in) {
local ($k, $v) = split(/=/, $i, 2);
$k =~ s/\+/ /g; $k =~ s/%(..)/pack("c",hex($1))/ge;
$v =~ s/\+/ /g; $v =~ s/%(..)/pack("c",hex($1))/ge;
$in{$k} = $v;
}
}
}
$posted_data = undef;
if ($method eq 'POST' &&
$header{'content-type'} eq 'application/x-www-form-urlencoded') {
# Read in posted query string information, up the configured maximum
# post request length
$clen = $header{"content-length"};
$clen_read = $clen > $config{'max_post'} ? $config{'max_post'} : $clen;
while(length($posted_data) < $clen_read) {
alarm(60);
$SIG{'ALRM'} = sub { die "timeout" };
eval {
$buf = &read_data($clen_read - length($posted_data));
};
alarm(0);
if ($@) {
&http_error(500, "Timeout reading POST request");
}
if (!length($buf)) {
&http_error(500, "Failed to read POST request");
}
chomp($posted_data);
$posted_data =~ s/\015$//mg;
$posted_data .= $buf;
}
print DEBUG "clen_read=$clen_read clen=$clen posted_data=",length($posted_data),"\n";
if ($clen_read != $clen && length($posted_data) > $clen) {
# If the client sent more data than we asked for, chop the
# rest off
$posted_data = substr($posted_data, 0, $clen);
}
if (length($posted_data) > $clen) {
# When the client sent too much, delay so that it gets headers
sleep(3);
}
if ($header{'user-agent'} =~ /MSIE/ &&
$header{'user-agent'} !~ /Opera/i) {
# MSIE includes an extra newline in the data
$posted_data =~ s/\r|\n//g;
}
local @in = split(/\&/, $posted_data);
foreach $i (@in) {
local ($k, $v) = split(/=/, $i, 2);
#$v =~ s/\r|\n//g;
$k =~ s/\+/ /g; $k =~ s/%(..)/pack("c",hex($1))/ge;
$v =~ s/\+/ /g; $v =~ s/%(..)/pack("c",hex($1))/ge;
$in{$k} = $v;
}
print DEBUG "handle_request: posted_data=$posted_data\n";
}
# Reject CONNECT request, which isn't supported
if ($method eq "CONNECT" || $method eq "TRACE") {
&http_error(405, "Method ".&html_strip($method)." is not supported");
}
# work out accepted encodings
%acceptenc = map { $_, 1 } split(/,/, $header{'accept-encoding'});
# replace %XX sequences in page
$page =~ s/%(..)/pack("c",hex($1))/ge;
# Check if the browser's user agent indicates a mobile device
$mobile_device = &is_mobile_useragent($header{'user-agent'});
# Check if Host: header is for a mobile URL
foreach my $m (@mobile_prefixes) {
if ($header{'host'} =~ /^\Q$m\E/i) {
$mobile_device = 1;
}
}
# check for the logout flag file, and if existent deny authentication
if ($config{'logout'} && -r $config{'logout'}.$in{'miniserv_logout_id'}) {
print DEBUG "handle_request: logout flag set\n";
$deny_authentication++;
open(LOGOUT, $config{'logout'}.$in{'miniserv_logout_id'});
chop($count = );
close(LOGOUT);
$count--;
if ($count > 0) {
open(LOGOUT, ">$config{'logout'}$in{'miniserv_logout_id'}");
print LOGOUT "$count\n";
close(LOGOUT);
}
else {
unlink($config{'logout'}.$in{'miniserv_logout_id'});
}
}
# check for any redirect for the requested URL
foreach my $pfx (@strip_prefix) {
my $l = length($pfx);
if(length($page) >= $l &&
substr($page,0,$l) eq $pfx) {
$page=substr($page,$l);
last;
}
}
$simple = &simplify_path($page, $bogus);
$rpath = $simple;
$rpath .= "&".$querystring if (defined($querystring));
$redir = $redirect{$rpath};
if (defined($redir)) {
print DEBUG "handle_request: redir=$redir\n";
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_data("Location: $prot://$hostport$redir\r\n");
&write_keep_alive(0);
&write_data("\r\n");
return 0;
}
# Check for a DAV request
$davpath = undef;
foreach my $d (@davpaths) {
if ($simple eq $d || $simple =~ /^\Q$d\E\//) {
$davpath = $d;
last;
}
}
if (!$davpath && ($method eq "SEARCH" || $method eq "PUT")) {
&http_error(400, "Bad Request method ".&html_strip($method));
}
# Check for some form of authentication
print DEBUG "handle_request: Need authentication\n";
$validated = 0;
$blocked = 0;
# Session authentication is never used for connections by
# another webmin server, or for specified pages, or for DAV, or XMLRPC,
# or mobile browsers if requested.
if ($header{'user-agent'} =~ /webmin/i ||
$header{'user-agent'} =~ /$config{'agents_nosession'}/i ||
$sessiononly{$simple} || $davpath ||
$simple eq "/xmlrpc.cgi" ||
$acptip eq $config{'host_nosession'} ||
$mobile_device && $config{'mobile_nosession'}) {
print DEBUG "handle_request: Forcing HTTP authentication\n";
$config{'session'} = 0;
}
# Check for SSL authentication
my $trust_ssl = $config{'trust_real_ip'} && !$config{'no_trust_ssl'};
if ($use_ssl && $verified_client ||
$trust_ssl && $header{'x-ssl-client-dn'}) {
if ($use_ssl && $verified_client) {
$peername = Net::SSLeay::X509_NAME_oneline(
Net::SSLeay::X509_get_subject_name(
Net::SSLeay::get_peer_certificate(
$ssl_con)));
$u = &find_user_by_cert($peername);
}
if ($trust_ssl && !$u && $header{'x-ssl-client-dn'}) {
# Use proxied client cert
$u = &find_user_by_cert($header{'x-ssl-client-dn'});
}
if ($u) {
$authuser = $u;
$validated = 2;
}
if ($use_syslog && !$validated && $use_ssl && $verified_client) {
syslog("crit", "%s",
"Unknown SSL certificate $peername");
}
}
if (!$validated && !$deny_authentication) {
# check for IP-based authentication
local $a;
foreach $a (keys %ipaccess) {
if ($acptip eq $a) {
# It does! Auth as the user
$validated = 3;
$baseauthuser = $authuser =
$ipaccess{$a};
}
}
}
# Check for normal HTTP authentication
if (!$validated && !$deny_authentication && !$config{'session'} &&
$header{authorization} =~ /^basic\s+(\S+)$/i) {
# authorization given..
($authuser, $authpass) = split(/:/, &b64decode($1), 2);
print DEBUG "handle_request: doing basic auth check authuser=$authuser authpass=$authpass\n";
local ($vu, $expired, $nonexist, $wvu) =
&validate_user_caseless($authuser, $authpass, $host,
$acptip, $port);
print DEBUG "handle_request: vu=$vu expired=$expired nonexist=$nonexist\n";
if ($vu && (!$expired || $config{'passwd_mode'} == 1)) {
$authuser = $vu;
$validated = 1;
}
else {
$validated = 0;
}
if ($use_syslog && !$validated) {
syslog("crit", "%s",
($nonexist ? "Non-existent" :
$expired ? "Expired" : "Invalid").
" login as $authuser from $acpthost");
}
if ($authuser =~ /\r|\n|\s/) {
&http_error(500, "Invalid username",
"Username contains invalid characters");
}
if ($authpass =~ /\r|\n/) {
&http_error(500, "Invalid password",
"Password contains invalid characters");
}
if ($config{'passdelay'} && $authuser) {
# check with main process for delay
print DEBUG "handle_request: about to ask for password delay\n";
print $PASSINw "delay $authuser $acptip $validated\n";
<$PASSOUTr> =~ /(\d+) (\d+)/;
$blocked = $2;
print DEBUG "handle_request: password delay $1 $2\n";
sleep($1);
}
}
# Check for a visit to the special session login page
if ($config{'session'} && !$deny_authentication &&
$page eq $config{'session_login'}) {
if ($in{'logout'} && $header{'cookie'} =~ /(^|\s|;)$sidname=([a-f0-9]+)/) {
# Logout clicked .. remove the session
local $sid = $2;
print $PASSINw "delete $sid\n";
local $louser = <$PASSOUTr>;
chop($louser);
$logout = 1;
$already_session_id = undef;
$authuser = $baseauthuser = undef;
if ($louser) {
if ($use_syslog) {
syslog("info", "%s", "Logout by $louser from $acpthost");
}
&run_logout_script($louser, $sid,
$loghost, $localip);
&write_logout_utmp($louser, $actphost);
}
}
elsif ($in{'session'}) {
# Session ID given, perhaps from a single-use login link.
local $sid = $in{'session'};
if ($sid =~ /\r|\n|\s/) {
&http_error(500, "Invalid session",
"Session ID contains invalid characters");
}
print $PASSINw "verify $sid $acptip 1\n";
<$PASSOUTr> =~ /^(\d+)\s+(\S+)/;
if ($1 != 2) {
&http_error(500, "Invalid session",
"Session ID is not valid");
}
# If this was a one-time session ID link, the username will
# have a - prefix to prevent it from being used as a regular
# session.
local $vu = $2;
$vu =~ s/^-//;
# Clear this one-time session, and issue a new one
print $PASSINw "delete $sid\n";
local $louser = <$PASSOUTr>;
local $hrv = &handle_login(
$vu, $vu ? 1 : 0,
0, 0, undef, 1, 0);
return $hrv if (defined($hrv));
}
else {
# Trim username to remove leading and trailing spaces to
# be able to login, if username pastes from somewhere
$in{'user'} =~ s/^\s+|\s+$//g;
# Validate the user
if ($in{'user'} =~ /\r|\n|\s/) {
&run_failed_script($in{'user'}, 'baduser',
$loghost, $localip);
&http_error(500, "Invalid username",
"Username contains invalid characters");
}
if ($in{'pass'} =~ /\r|\n/) {
&run_failed_script($in{'user'}, 'badpass',
$loghost, $localip);
&http_error(500, "Invalid password",
"Password contains invalid characters");
}
local $twofactor_probe = 0;
local ($vu, $expired, $nonexist, $wvu) =
&validate_user_caseless($in{'user'}, $in{'pass'}, $host,
$acptip, $port);
if ($vu && $wvu) {
my $uinfo = &get_user_details($wvu, $vu);
my $can2fa = $uinfo && $uinfo->{'twofactor_provider'};
$twofactor_probe = 1 if ($in{'twofprobe'} && $can2fa);
if ($can2fa && !$twofactor_probe) {
# Check two-factor token ID
$err = &validate_twofactor(
$wvu, $in{'twofactor'}, $vu);
if ($err) {
&run_failed_script(
$vu, 'twofactor',
$loghost, $localip);
$twofactor_msg = $err;
$twofactor_nolog = 'nolog'
if (!$in{'twofactor'});
$vu = undef;
}
}
}
local $hrv = &handle_login(
$vu || $in{'user'}, $vu ? 1 : 0,
$expired, $nonexist, $in{'pass'},
$in{'notestingcookie'}, $twofactor_nolog,
$twofactor_probe);
return $hrv if (defined($hrv));
}
}
# Check for a visit to the special PAM login page
if ($config{'session'} && !$deny_authentication &&
$use_pam && $config{'pam_conv'} && $page eq $config{'pam_login'} &&
!$in{'restart'}) {
# A question has been entered .. submit it to the main process
print DEBUG "handle_request: Got call to $page ($in{'cid'})\n";
print DEBUG "handle_request: For PAM, authuser=$authuser\n";
if ($in{'answer'} =~ /\r|\n/ || $in{'cid'} =~ /\r|\n|\s/) {
&http_error(500, "Invalid response",
"Response contains invalid characters");
}
if (!$in{'cid'}) {
# Start of a new conversation - answer must be username
$cid = &generate_random_id();
print $PASSINw "pamstart $cid $host $in{'answer'}\n";
}
else {
# A response to a previous question
$cid = $in{'cid'};
print $PASSINw "pamanswer $cid $in{'answer'}\n";
}
# Read back the response, and the next question (if any)
local $line = <$PASSOUTr>;
$line =~ s/\r|\n//g;
local ($rv, $question) = split(/\s+/, $line, 2);
if ($rv == 0) {
# Cannot login!
local $hrv = &handle_login(
!$in{'cid'} && $in{'answer'} ? $in{'answer'}
: "unknown",
0, 0, 1, undef);
return $hrv if (defined($hrv));
}
elsif ($rv == 1 || $rv == 3) {
# Another question .. force use of PAM CGI
$validated = 1;
$method = "GET";
$querystring .= "&cid=$cid&question=".
&urlize($question);
$querystring .= "&password=1" if ($rv == 3);
$queryargs = "";
$page = $config{'pam_login'};
$miniserv_internal = 1;
$logged_code = 401;
}
elsif ($rv == 2) {
# Got back a final ok or failure
local ($user, $ok, $expired, $nonexist) =
split(/\s+/, $question);
local $hrv = &handle_login(
$user, $ok, $expired, $nonexist, undef,
$in{'notestingcookie'});
return $hrv if (defined($hrv));
}
elsif ($rv == 4) {
# A message from PAM .. tell the user
$validated = 1;
$method = "GET";
$querystring .= "&cid=$cid&message=".
&urlize($question);
$queryargs = "";
$page = $config{'pam_login'};
$miniserv_internal = 1;
$logged_code = 401;
}
}
# Check for a visit to the special password change page
if ($config{'session'} && !$deny_authentication &&
$page eq $config{'password_change'} && !$validated) {
# Just let this slide ..
$validated = 1;
$miniserv_internal = 3;
# check with main process for delay
if ($config{'passdelay'}) {
print DEBUG "handle_request: requesting delay acptip=$acptip\n";
print $PASSINw "delay - $acptip 0\n";
<$PASSOUTr> =~ /(\d+) (\d+)/;
sleep($1);
print DEBUG "handle_request: delay=$1 blocked=$2\n";
}
}
# Check for an existing session
if ($config{'session'} && !$validated) {
if ($already_session_id) {
$session_id = $already_session_id;
$authuser = $already_authuser;
$validated = 1;
}
elsif (!$deny_authentication &&
$header{'cookie'} =~ /(^|\s|;)$sidname=([a-f0-9]+)/) {
# Try all session cookies
local $cookie = $header{'cookie'};
while($cookie =~ s/(^|\s|;)$sidname=([a-f0-9]+)//) {
$session_id = $2;
print $PASSINw "verify $session_id $acptip 1\n";
<$PASSOUTr> =~ /(\d+)\s+(\S+)/;
if ($1 == 2) {
# Valid session continuation
$validated = 1;
$authuser = $2;
$already_authuser = $authuser;
$timed_out = undef;
last;
}
elsif ($1 == 1) {
# Session timed out
$timed_out = $2;
}
elsif ($1 == 3) {
# Session is OK, but from the wrong IP
&log_error("Session $session_id was ",
"used from $acptip instead of ",
"original IP $2");
}
else {
# Invalid session ID .. don't set
# verified flag
}
}
}
if ($authuser) {
# We got a session .. but does the user still exist?
my @can = &can_user_login($authuser, undef, $host);
$baseauthuser = $can[3] || $authuser;
my $auser = &get_user_details($baseauthuser, $authuser);
if (!$auser) {
&log_error("Session $session_id is for user ",
"$authuser who does not exist");
$validated = 0;
$already_authuser = $authuser = undef;
}
}
}
# Check for local authentication
if ($localauth_user && !$header{'x-forwarded-for'} && !$header{'via'}) {
my $luser = &get_user_details($localauth_user);
if ($luser) {
# Local user exists in webmin users file
$validated = 1;
$authuser = $localauth_user;
}
else {
# Check if local user is allowed by unixauth
local @can = &can_user_login($localauth_user,
undef, $host);
if ($can[0]) {
$validated = 2;
$authuser = $localauth_user;
}
else {
$localauth_user = undef;
}
}
}
if (!$validated) {
# Check if this path allows anonymous access
local $a;
foreach $a (keys %anonymous) {
if (substr($simple, 0, length($a)) eq $a) {
# It does! Auth as the user, if IP access
# control allows him.
if (&check_user_ip($anonymous{$a}) &&
&check_user_time($anonymous{$a})) {
$validated = 3;
$baseauthuser = $authuser =
$anonymous{$a};
}
}
}
}
if (!$validated) {
# Check if this path allows unauthenticated access
my $unauth;
foreach my $u (@unauth) {
$unauth = 4 if ($simple =~ /$u/);
}
foreach my $u (@unauthcgi) {
$unauth = 3 if ($simple =~ /$u/);
}
if (!$bogus && $unauth) {
# Unauthenticated directory or file request - approve it
$validated = $unauth;
$baseauthuser = $authuser = undef;
}
}
if (!$validated) {
if ($blocked == 0) {
# No password given.. ask
if ($config{'pam_conv'} && $use_pam) {
# Force CGI for PAM question, starting with
# the username which is always needed
$validated = 1;
$method = "GET";
$querystring .= "&initial=1&question=".
&urlize("Username");
$querystring .= "&failed=$failed_user" if ($failed_user);
$querystring .= "&timed_out=$timed_out" if ($timed_out);
$queryargs = "";
$page = $config{'pam_login'};
$miniserv_internal = 1;
$logged_code = 401;
}
elsif ($config{'session'}) {
# Force CGI for session login
$validated = 1;
if ($logout) {
$querystring .= "&logout=1&page=/";
}
else {
# Re-direct to current module only
local $rpage = $request_uri;
if (!$config{'loginkeeppage'}) {
$rpage =~ s/\?.*$//;
$rpage =~ s/[^\/]+$//
}
$querystring = "page=".&urlize($rpage);
}
$method = "GET";
$querystring .= "&failed=".&urlize($failed_user)
if ($failed_user);
$querystring .= "&twofactor_msg=".&urlize($twofactor_msg)
if ($twofactor_msg);
$querystring .= "&timed_out=$timed_out"
if ($timed_out);
$queryargs = "";
$page = $config{'session_login'};
$miniserv_internal = 1;
$logged_code = 401;
}
else {
# Ask for login with HTTP authentication
&write_data("HTTP/1.0 401 Unauthorized\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_data("WWW-authenticate: Basic ".
"realm=\"$config{'realm'}\"\r\n");
&write_keep_alive(0);
&write_data("Content-type: text/html; Charset=utf-8\r\n");
&write_data("\r\n");
&reset_byte_count();
&write_data("\n");
&write_data("".&embed_error_styles($roots[0])."401 — Unauthorized\n");
&write_data("
401 — Unauthorized
\n");
&write_data("
A password is required to access this\n");
&write_data("web server. Please try again.
\n");
&write_data("\n");
&log_request($loghost, undef, $reqline, 401, &byte_count());
return 0;
}
}
elsif ($blocked == 1) {
# when the host has been blocked, give it an error
&http_error(403, "Access denied for $acptip. The host ".
"has been blocked because of too ".
"many authentication failures.");
}
elsif ($blocked == 2) {
# when the user has been blocked, give it an error
&http_error(403, "Access denied. The user ".
"has been blocked because of too ".
"many authentication failures.");
}
}
else {
# Get the real Webmin username
if (!$baseauthuser) {
local @can = &can_user_login($authuser, undef, $host);
$baseauthuser = $can[3] || $authuser;
}
if ($config{'remoteuser'} && !$< && $validated) {
# Switch to the UID of the remote user (if he exists)
local @u = getpwnam($authuser);
if (@u && $< != $u[2]) {
$( = $u[3]; $) = "$u[3] $u[3]";
($>, $<) = ($u[2], $u[2]);
}
else {
&http_error(500, "Unix user ".
&html_strip($authuser)." does not exist");
return 0;
}
}
}
# Check per-user IP access control
if (!&check_user_ip($baseauthuser)) {
&http_error(403, "Access denied for $acptip for ".
&html_strip($baseauthuser));
return 0;
}
# Check per-user allowed times
if (!&check_user_time($baseauthuser)) {
&http_error(403, "Access denied at the current time");
return 0;
}
$uinfo = &get_user_details($baseauthuser, $authuser);
# Validate the path, and convert to canonical form
rerun:
$simple = &simplify_path($page, $bogus);
print DEBUG "handle_request: page=$page simple=$simple\n";
if ($bogus) {
&http_error(400, "Invalid path");
return 0;
}
# Check for a DAV request
if ($davpath) {
return &handle_dav_request($davpath);
}
# Check for a websockets request
if (lc($header{'connection'}) =~ /upgrade/ &&
lc($header{'upgrade'}) eq 'websocket' &&
$baseauthuser) {
print DEBUG "websockets request to $simple\n";
my ($ws) = grep { $_->{'path'} eq $simple } @websocket_paths;
if (!$ws) {
&http_error(400, "Unknown websocket path");
return 0;
}
return &handle_websocket_request($ws, $simple);
}
# Work out the active theme(s)
local $preroots = $mobile_device && defined($config{'mobile_preroot'}) ?
$config{'mobile_preroot'} :
$authuser && defined($config{'preroot_'.$authuser}) ?
$config{'preroot_'.$authuser} :
$uinfo && defined($uinfo->{'preroot'}) ?
$uinfo->{'preroot'} :
$config{'preroot'};
local @preroots = reverse(split(/\s+/, $preroots));
# Canonicalize the directories
local @themes;
foreach my $preroot (@preroots) {
# Always under the current webmin root
$preroot =~ s/^.*\///g;
push(@themes, $preroot);
$preroot = $roots[0].'/'.$preroot;
}
# Look in the theme root directories first
local ($full, @stfull);
$foundroot = undef;
foreach my $preroot (@preroots) {
$is_directory = 1;
$sofar = "";
$full = $preroot.$sofar;
$scriptname = $simple;
foreach $b (split(/\//, $simple)) {
if ($b ne "") { $sofar .= "/$b"; }
$full = $preroot.$sofar;
@stfull = stat($full);
if (!@stfull) { undef($full); last; }
# Check if this is a directory
if (-d _) {
# It is.. go on parsing
$is_directory = 1;
next;
}
else {
$is_directory = 0;
}
# Check if this is a CGI program
if (&get_type($full) eq "internal/cgi") {
$pathinfo = substr($simple, length($sofar));
$pathinfo .= "/" if ($page =~ /\/$/);
$scriptname = $sofar;
last;
}
}
# Don't stop at a directory unless this is the last theme, which
# is the 'real' one that provides the .cgi scripts
if ($is_directory && $preroot ne $preroots[$#preroots]) {
next;
}
if ($full) {
# Found it!
if ($sofar eq '') {
$cgi_pwd = $roots[0];
}
elsif ($is_directory) {
$cgi_pwd = "$roots[0]$sofar";
}
else {
"$roots[0]$sofar" =~ /^(.*\/)[^\/]+$/;
$cgi_pwd = $1;
}
$foundroot = $preroot;
if ($is_directory) {
# Check for index files in the directory
local $foundidx;
foreach $idx (split(/\s+/, $config{"index_docs"})) {
$idxfull = "$full/$idx";
local @stidxfull = stat($idxfull);
if (-r _ && !-d _) {
$full = $idxfull;
@stfull = @stidxfull;
$is_directory = 0;
$scriptname .= "/"
if ($scriptname ne "/");
$foundidx++;
last;
}
}
@stfull = stat($full) if (!$foundidx);
}
}
last if ($foundroot);
}
print DEBUG "handle_request: initial full=$full\n";
# Look in the real root directories, stopping when we find a file or directory
if (!$full || $is_directory) {
ROOT: foreach $root (@roots) {
$sofar = "";
$full = $root.$sofar;
$scriptname = $simple;
foreach $b ($simple eq "/" ? ( "" ) : split(/\//, $simple)) {
if ($b ne "") { $sofar .= "/$b"; }
$full = $root.$sofar;
@stfull = stat($full);
if (!@stfull) {
next ROOT;
}
# Check if this is a directory
if (-d _) {
# It is.. go on parsing
next;
}
# Check if this is a CGI program
if (&get_type($full) eq "internal/cgi") {
$pathinfo = substr($simple, length($sofar));
$pathinfo .= "/" if ($page =~ /\/$/);
$scriptname = $sofar;
last;
}
}
# Run CGI in the same directory as whatever file
# was requested
$full =~ /^(.*\/)[^\/]+$/; $cgi_pwd = $1;
if (-e $full) {
# Found something!
$realroot = $root;
$foundroot = $root;
last;
}
}
if (!@stfull) { &http_error(404, "File not found"); }
}
print DEBUG "handle_request: full=$full\n";
@stfull = stat($full) if (!@stfull);
# check filename against denyfile regexp
local $denyfile = $config{'denyfile'};
if ($denyfile && $full =~ /$denyfile/) {
&http_error(403, "Access denied to ".&html_strip($page));
return 0;
}
# Reached the end of the path OK.. see what we've got
if (-d _) {
# See if the URL ends with a / as it should
print DEBUG "handle_request: found a directory\n";
if ($page !~ /\/$/) {
# It doesn't.. redirect
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_data("Location: $prot://$hostport$page/\r\n");
&write_keep_alive(0);
&write_data("\r\n");
&log_request($loghost, $authuser, $reqline, 302, 0);
return 0;
}
# A directory.. check for index files
local $foundidx;
foreach $idx (split(/\s+/, $config{"index_docs"})) {
$idxfull = "$full/$idx";
@stidxfull = stat($idxfull);
if (-r _ && !-d _) {
$cgi_pwd = $full;
$full = $idxfull;
@stfull = @stidxfull;
$scriptname .= "/" if ($scriptname ne "/");
$foundidx++;
last;
}
}
@stfull = stat($full) if (!$foundidx);
}
if (-d _) {
# This is definitely a directory.. list it
if ($config{'nolistdir'}) {
&http_error(500, "Directory is missing an index file");
}
print DEBUG "handle_request: listing directory\n";
local $resp = "HTTP/1.0 $ok_code $ok_message\r\n".
"Date: $datestr\r\n".
"Server: @{[&server_info()]}\r\n".
"Content-type: text/html; Charset=utf-8\r\n";
&write_data($resp);
&write_keep_alive(0);
&write_data("\r\n");
&reset_byte_count();
&write_data("".&embed_error_styles($roots[0])."