1#!/usr/local/bin/perl 2# A very simple perl web server used by Webmin 3 4# Require basic libraries 5package miniserv; 6use Socket; 7use POSIX; 8use Time::Local; 9eval "use Time::HiRes;"; 10eval "use Socket6;"; 11 12@itoa64 = split(//, "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); 13 14# Find and read config file 15if ($ARGV[0] eq "--nofork") { 16 $nofork_argv = 1; 17 shift(@ARGV); 18 } 19if (@ARGV != 1) { 20 die "Usage: miniserv.pl <config file>"; 21 } 22if ($ARGV[0] =~ /^([a-z]:)?\//i) { 23 $config_file = $ARGV[0]; 24 } 25else { 26 chop($pwd = `pwd`); 27 $config_file = "$pwd/$ARGV[0]"; 28 } 29%config = &read_config_file($config_file); 30if ($config{'perllib'}) { 31 push(@INC, split(/:/, $config{'perllib'})); 32 $ENV{'PERLLIB'} .= ':'.$config{'perllib'}; 33 } 34@startup_msg = ( ); 35 36# Check if SSL is enabled and available 37if ($config{'ssl'}) { 38 eval "use Net::SSLeay"; 39 if (!$@) { 40 $use_ssl = 1; 41 # These functions only exist for SSLeay 1.0 42 eval "Net::SSLeay::SSLeay_add_ssl_algorithms()"; 43 eval "Net::SSLeay::load_error_strings()"; 44 if (defined(&Net::SSLeay::X509_STORE_CTX_get_current_cert) && 45 defined(&Net::SSLeay::CTX_load_verify_locations) && 46 (defined(&Net::SSLeay::CTX_set_verify) || 47 defined(&Net::SSLeay::set_verify))) { 48 $client_certs = 1; 49 } 50 } 51 } 52 53# Check if IPv6 is enabled and available 54eval "use Socket6"; 55$socket6err = $@; 56if ($config{'ipv6'}) { 57 if (!$socket6err) { 58 push(@startup_msg, "IPv6 support enabled"); 59 $use_ipv6 = 1; 60 } 61 else { 62 push(@startup_msg, "IPv6 support cannot be enabled without ". 63 "the Socket6 perl module"); 64 } 65 } 66 67# Check if the syslog module is available to log hacking attempts 68if ($config{'syslog'} && !$config{'inetd'}) { 69 eval "use Sys::Syslog qw(:DEFAULT setlogsock)"; 70 if (!$@) { 71 $use_syslog = 1; 72 } 73 } 74 75# check if the TCP-wrappers module is available 76if ($config{'libwrap'}) { 77 eval "use Authen::Libwrap qw(hosts_ctl STRING_UNKNOWN)"; 78 if (!$@) { 79 $use_libwrap = 1; 80 } 81 } 82 83# Check if the MD5 perl module is available 84eval "use MD5; \$dummy = new MD5; \$dummy->add('foo');"; 85if (!$@) { 86 $use_md5 = "MD5"; 87 } 88else { 89 eval "use Digest::MD5; \$dummy = new Digest::MD5; \$dummy->add('foo');"; 90 if (!$@) { 91 $use_md5 = "Digest::MD5"; 92 } 93 } 94if ($use_md5) { 95 push(@startup_msg, "Using MD5 module $use_md5"); 96 } 97 98# Check if the SHA512 perl module is available 99eval "use Crypt::SHA"; 100$use_sha512 = $@ ? "Crypt::SHA" : undef; 101if ($use_sha512) { 102 push(@startup_msg, "Using SHA512 module $use_sha512"); 103 } 104 105# Get miniserv's perl path and location 106$miniserv_path = $0; 107open(SOURCE, $miniserv_path); 108<SOURCE> =~ /^#!(\S+)/; 109$perl_path = $1; 110close(SOURCE); 111if (!-x $perl_path) { 112 $perl_path = $^X; 113 } 114if (-l $perl_path) { 115 $linked_perl_path = readlink($perl_path); 116 } 117@miniserv_argv = @ARGV; 118 119# Check vital config options 120&update_vital_config(); 121 122# Check if already running via the PID file 123if (open(PIDFILE, $config{'pidfile'})) { 124 my $already = <PIDFILE>; 125 close(PIDFILE); 126 chop($already); 127 if ($already && $already != $$ && kill(0, $already)) { 128 die "Webmin is already running with PID $already\n"; 129 } 130 } 131 132$sidname = $config{'sidname'}; 133die "Session authentication cannot be used in inetd mode" 134 if ($config{'inetd'} && $config{'session'}); 135 136# check if the PAM module is available to authenticate 137if ($config{'assume_pam'}) { 138 # Just assume that it will work. This can also be used to work around 139 # a Solaris bug in which using PAM before forking caused it to fail 140 # later! 141 $use_pam = 1; 142 } 143elsif (!$config{'no_pam'}) { 144 eval "use Authen::PAM;"; 145 if (!$@) { 146 # check if the PAM authentication can be used by opening a 147 # PAM handle 148 local $pamh; 149 if (ref($pamh = new Authen::PAM($config{'pam'}, 150 $config{'pam_test_user'}, 151 \&pam_conv_func))) { 152 # Now test a login to see if /etc/pam.d/webmin is set 153 # up properly. 154 $pam_conv_func_called = 0; 155 $pam_username = "test"; 156 $pam_password = "test"; 157 $pamh->pam_authenticate(); 158 if ($pam_conv_func_called) { 159 push(@startup_msg, 160 "PAM authentication enabled"); 161 $use_pam = 1; 162 } 163 else { 164 push(@startup_msg, 165 "PAM test failed - maybe ". 166 "/etc/pam.d/$config{'pam'} does not exist"); 167 } 168 } 169 else { 170 push(@startup_msg, 171 "PAM initialization of Authen::PAM failed"); 172 } 173 } 174 } 175if ($config{'pam_only'} && !$use_pam) { 176 foreach $msg (@startup_msg) { 177 print STDERR $msg,"\n"; 178 } 179 print STDERR "PAM use is mandatory, but could not be enabled!\n"; 180 print STDERR "no_pam and pam_only both are set!\n" if ($config{no_pam}); 181 exit(1); 182 } 183elsif ($pam_msg && !$use_pam) { 184 push(@startup_msg, 185 "Continuing without the Authen::PAM perl module"); 186 } 187 188# Check if the User::Utmp perl module is installed 189if ($config{'utmp'}) { 190 eval "use User::Utmp;"; 191 if (!$@) { 192 $write_utmp = 1; 193 push(@startup_msg, "UTMP logging enabled"); 194 } 195 else { 196 push(@startup_msg, 197 "Perl module User::Utmp needed for Utmp logging is ". 198 "not installed : $@"); 199 } 200 } 201 202# See if the crypt function fails 203eval "crypt('foo', 'xx')"; 204if ($@) { 205 eval "use Crypt::UnixCrypt"; 206 if (!$@) { 207 $use_perl_crypt = 1; 208 push(@startup_msg, 209 "Using Crypt::UnixCrypt for password encryption"); 210 } 211 else { 212 push(@startup_msg, 213 "crypt() function un-implemented, and Crypt::UnixCrypt ". 214 "not installed - password authentication will fail"); 215 } 216 } 217 218# Check if /dev/urandom really generates random IDs, by calling it twice 219local $rand1 = &generate_random_id(1); 220local $rand2 = &generate_random_id(1); 221if ($rand1 eq $rand2) { 222 $bad_urandom = 1; 223 push(@startup_msg, 224 "Random number generator file /dev/urandom is not reliable"); 225 } 226 227# Check if we can call sudo 228if ($config{'sudo'} && &has_command("sudo")) { 229 eval "use IO::Pty"; 230 if (!$@) { 231 $use_sudo = 1; 232 } 233 else { 234 push(@startup_msg, 235 "Perl module IO::Pty needed for calling sudo is not ". 236 "installed : $@"); 237 } 238 } 239 240# init days and months for http_date 241@weekday = ( "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ); 242@month = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun", 243 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ); 244 245# Change dir to the server root 246@roots = ( $config{'root'} ); 247for($i=0; defined($config{"extraroot_$i"}); $i++) { 248 push(@roots, $config{"extraroot_$i"}); 249 } 250chdir($roots[0]); 251eval { $user_homedir = (getpwuid($<))[7]; }; 252if ($@) { 253 # getpwuid doesn't work on windows 254 $user_homedir = $ENV{"HOME"} || $ENV{"USERPROFILE"} || "/"; 255 $on_windows = 1; 256 } 257 258# Read users file 259&read_users_file(); 260 261# Setup SSL if possible and if requested 262if (!-r $config{'keyfile'}) { 263 # Key file doesn't exist! 264 if ($config{'keyfile'}) { 265 print STDERR "SSL key file $config{'keyfile'} does not exist\n"; 266 } 267 $use_ssl = 0; 268 } 269elsif ($config{'certfile'} && !-r $config{'certfile'}) { 270 # Cert file doesn't exist! 271 print STDERR "SSL cert file $config{'certfile'} does not exist\n"; 272 $use_ssl = 0; 273 } 274@ipkeys = &get_ipkeys(\%config); 275if ($use_ssl) { 276 if ($config{'ssl_version'}) { 277 # Force an SSL version 278 $Net::SSLeay::version = $config{'ssl_version'}; 279 $Net::SSLeay::ssl_version = $config{'ssl_version'}; 280 } 281 $client_certs = 0 if (!-r $config{'ca'} || !%certs); 282 $ctx = &create_ssl_context($config{'keyfile'}, 283 $config{'certfile'}, 284 $config{'extracas'}); 285 $ctx || die "Failed to create default SSL context"; 286 $ssl_contexts{"*"} = $ctx; 287 foreach $ipkey (@ipkeys) { 288 $ctx = &create_ssl_context($ipkey->{'key'}, $ipkey->{'cert'}, 289 $ipkey->{'extracas'} || $config{'extracas'}); 290 if ($ctx) { 291 foreach $ip (@{$ipkey->{'ips'}}) { 292 $ssl_contexts{$ip} = $ctx; 293 } 294 } 295 } 296 297 # Setup per-hostname SSL contexts on the main IP 298 if (defined(&Net::SSLeay::CTX_set_tlsext_servername_callback)) { 299 Net::SSLeay::CTX_set_tlsext_servername_callback( 300 $ssl_contexts{"*"}, 301 sub { 302 my $ssl = shift; 303 my $h = Net::SSLeay::get_servername($ssl); 304 my $c = $ssl_contexts{$h} || 305 $h =~ /^[^\.]+\.(.*)$/ && $ssl_contexts{"*.$1"}; 306 if ($c) { 307 Net::SSLeay::set_SSL_CTX($ssl, $c); 308 } 309 }); 310 } 311 } 312 313# Load gzip library if enabled 314if ($config{'gzip'} eq '1') { 315 eval "use Compress::Zlib"; 316 if (!$@) { 317 $use_gzip = 1; 318 } 319 } 320 321# Setup syslog support if possible and if requested 322if ($use_syslog) { 323 open(ERRDUP, ">&STDERR"); 324 open(STDERR, ">/dev/null"); 325 $log_socket = $config{"logsock"} || "unix"; 326 eval 'openlog($config{"pam"}, "cons,pid,ndelay", "authpriv"); setlogsock($log_socket)'; 327 if ($@) { 328 $use_syslog = 0; 329 } 330 else { 331 local $msg = ucfirst($config{'pam'})." starting"; 332 eval { syslog("info", "%s", $msg); }; 333 if ($@) { 334 eval { 335 setlogsock("inet"); 336 syslog("info", "%s", $msg); 337 }; 338 if ($@) { 339 # All attempts to use syslog have failed.. 340 $use_syslog = 0; 341 } 342 } 343 } 344 open(STDERR, ">&ERRDUP"); 345 close(ERRDUP); 346 } 347 348# Read MIME types file and add extra types 349&read_mime_types(); 350 351# get the time zone 352if ($config{'log'}) { 353 local(@gmt, @lct, $days, $hours, $mins); 354 @gmt = gmtime(time()); 355 @lct = localtime(time()); 356 $days = $lct[3] - $gmt[3]; 357 $hours = ($days < -1 ? 24 : 1 < $days ? -24 : $days * 24) + 358 $lct[2] - $gmt[2]; 359 $mins = $hours * 60 + $lct[1] - $gmt[1]; 360 $timezone = ($mins < 0 ? "-" : "+"); $mins = abs($mins); 361 $timezone .= sprintf "%2.2d%2.2d", $mins/60, $mins%60; 362 } 363 364# Build various maps from the config files 365&build_config_mappings(); 366 367# start up external authentication program, if needed 368if ($config{'extauth'}) { 369 socketpair(EXTAUTH, EXTAUTH2, AF_UNIX, SOCK_STREAM, PF_UNSPEC); 370 if (!($extauth = fork())) { 371 close(EXTAUTH); 372 close(STDIN); 373 close(STDOUT); 374 open(STDIN, "<&EXTAUTH2"); 375 open(STDOUT, ">&EXTAUTH2"); 376 exec($config{'extauth'}) or die "exec failed : $!\n"; 377 } 378 close(EXTAUTH2); 379 local $os = select(EXTAUTH); 380 $| = 1; select($os); 381 } 382 383# Pre-load any libraries 384if (!$config{'inetd'}) { 385 foreach $pl (split(/\s+/, $config{'preload'})) { 386 ($pkg, $lib) = split(/=/, $pl); 387 $pkg =~ s/[^A-Za-z0-9]/_/g; 388 eval "package $pkg; do '$config{'root'}/$lib'"; 389 if ($@) { 390 print STDERR "Failed to pre-load $lib in $pkg : $@\n"; 391 } 392 } 393 foreach $pl (split(/\s+/, $config{'premodules'})) { 394 if ($pl =~ /\//) { 395 ($dir, $mod) = split(/\//, $pl); 396 } 397 else { 398 ($dir, $mod) = (undef, $pl); 399 } 400 push(@INC, "$config{'root'}/$dir"); 401 eval "package $mod; use $mod ()"; 402 if ($@) { 403 print STDERR "Failed to pre-load $mod : $@\n"; 404 } 405 } 406 foreach $mod (split(/\s+/, $config{'preuse'})) { 407 eval "use $mod;"; 408 if ($@) { 409 print STDERR "Failed to pre-load $mod : $@\n"; 410 } 411 } 412 } 413 414# Open debug log if set 415if ($config{'debuglog'}) { 416 open(DEBUG, ">>$config{'debuglog'}"); 417 chmod(0700, $config{'debuglog'}); 418 select(DEBUG); $| = 1; select(STDOUT); 419 print DEBUG "miniserv.pl starting ..\n"; 420 } 421 422# Write out (empty) blocked hosts file 423&write_blocked_file(); 424 425# Initially read webmin cron functions and last execution times 426&read_webmin_crons(); 427%webmincron_last = ( ); 428&read_file($config{'webmincron_last'}, \%webmincron_last); 429 430# Pre-cache lang files 431&precache_files(); 432 433# Clear any flag files to prevent restart loops 434unlink($config{'restartflag'}) if ($config{'restartflag'}); 435unlink($config{'reloadflag'}) if ($config{'reloadflag'}); 436unlink($config{'stopflag'}) if ($config{'stopflag'}); 437 438if ($config{'inetd'}) { 439 # We are being run from inetd - go direct to handling the request 440 &redirect_stderr_to_log(); 441 $SIG{'HUP'} = 'IGNORE'; 442 $SIG{'TERM'} = 'DEFAULT'; 443 $SIG{'PIPE'} = 'DEFAULT'; 444 open(SOCK, "+>&STDIN"); 445 446 # Check if it is time for the logfile to be cleared 447 if ($config{'logclear'}) { 448 local $write_logtime = 0; 449 local @st = stat("$config{'logfile'}.time"); 450 if (@st) { 451 if ($st[9]+$config{'logtime'}*60*60 < time()){ 452 # need to clear log 453 $write_logtime = 1; 454 unlink($config{'logfile'}); 455 } 456 } 457 else { $write_logtime = 1; } 458 if ($write_logtime) { 459 open(LOGTIME, ">$config{'logfile'}.time"); 460 print LOGTIME time(),"\n"; 461 close(LOGTIME); 462 } 463 } 464 465 # Work out if IPv6 is being used locally 466 local $sn = getsockname(SOCK); 467 print DEBUG "sn=$sn\n"; 468 print DEBUG "length=",length($sn),"\n"; 469 $localipv6 = length($sn) > 16; 470 print DEBUG "localipv6=$localipv6\n"; 471 472 # Initialize SSL for this connection 473 if ($use_ssl) { 474 $ssl_con = &ssl_connection_for_ip(SOCK, $localipv6); 475 $ssl_con || exit; 476 } 477 478 # Work out the hostname for this web server 479 $host = &get_socket_name(SOCK, $localipv6); 480 print DEBUG "host=$host\n"; 481 $host || exit; 482 $port = $config{'port'}; 483 $acptaddr = getpeername(SOCK); 484 print DEBUG "acptaddr=$acptaddr\n"; 485 print DEBUG "length=",length($acptaddr),"\n"; 486 $acptaddr || exit; 487 488 # Work out remote and local IPs 489 $ipv6 = length($acptaddr) > 16; 490 print DEBUG "ipv6=$ipv6\n"; 491 (undef, $locala) = &get_socket_ip(SOCK, $localipv6); 492 print DEBUG "locala=$locala\n"; 493 (undef, $peera, undef) = &get_address_ip($acptaddr, $ipv6); 494 print DEBUG "peera=$peera\n"; 495 496 print DEBUG "main: Starting handle_request loop pid=$$\n"; 497 while(&handle_request($peera, $locala, $ipv6)) { } 498 print DEBUG "main: Done handle_request loop pid=$$\n"; 499 close(SOCK); 500 exit; 501 } 502 503# Build list of sockets to listen on 504@listening_on_ports = (); 505$config{'bind'} = '' if ($config{'bind'} eq '*'); 506if ($config{'bind'}) { 507 # Listening on a specific IP 508 if (&check_ip6address($config{'bind'})) { 509 # IP is v6 510 $use_ipv6 || die "Cannot bind to $config{'bind'} without IPv6"; 511 push(@sockets, [ inet_pton(AF_INET6(),$config{'bind'}), 512 $config{'port'}, 513 PF_INET6() ]); 514 } 515 else { 516 # IP is v4 517 push(@sockets, [ inet_aton($config{'bind'}), 518 $config{'port'}, 519 PF_INET() ]); 520 } 521 } 522else { 523 # Listening on all IPs 524 push(@sockets, [ INADDR_ANY, $config{'port'}, PF_INET() ]); 525 if ($use_ipv6) { 526 # Also IPv6 527 push(@sockets, [ in6addr_any(), $config{'port'}, 528 PF_INET6() ]); 529 } 530 } 531foreach $s (split(/\s+/, $config{'sockets'})) { 532 if ($s =~ /^(\d+)$/) { 533 # Just listen on another port on the main IP 534 push(@sockets, [ $sockets[0]->[0], $s, $sockets[0]->[2] ]); 535 if ($use_ipv6 && !$config{'bind'}) { 536 # Also listen on that port on the main IPv6 address 537 push(@sockets, [ $sockets[1]->[0], $s, 538 $sockets[1]->[2] ]); 539 } 540 } 541 elsif ($s =~ /^\*:(\d+)$/) { 542 # Listening on all IPs on some port 543 push(@sockets, [ INADDR_ANY, $1, 544 PF_INET() ]); 545 if ($use_ipv6) { 546 push(@sockets, [ in6addr_any(), $1, 547 PF_INET6() ]); 548 } 549 } 550 elsif ($s =~ /^(\S+):(\d+)$/) { 551 # Listen on a specific port and IP 552 my ($ip, $port) = ($1, $2); 553 if (&check_ip6address($ip)) { 554 $use_ipv6 || die "Cannot bind to $ip without IPv6"; 555 push(@sockets, [ inet_pton(AF_INET6(), 556 $ip), 557 $port, PF_INET6() ]); 558 } 559 else { 560 push(@sockets, [ inet_aton($ip), $port, 561 PF_INET() ]); 562 } 563 } 564 elsif ($s =~ /^([0-9\.]+):\*$/ || $s =~ /^([0-9\.]+)$/) { 565 # Listen on the main port on another IPv4 address 566 push(@sockets, [ inet_aton($1), $sockets[0]->[1], 567 PF_INET() ]); 568 } 569 elsif (($s =~ /^([0-9a-f\:]+):\*$/ || $s =~ /^([0-9a-f\:]+)$/) && 570 $use_ipv6) { 571 # Listen on the main port on another IPv6 address 572 push(@sockets, [ inet_pton(AF_INET6(), $1), 573 $sockets[0]->[1], 574 PF_INET6() ]); 575 } 576 } 577 578# Open all the sockets 579$proto = getprotobyname('tcp'); 580@sockerrs = ( ); 581$tried_inaddr_any = 0; 582for($i=0; $i<@sockets; $i++) { 583 $fh = "MAIN$i"; 584 if (!socket($fh, $sockets[$i]->[2], SOCK_STREAM, $proto)) { 585 # Protocol not supported 586 push(@sockerrs, "Failed to open socket family $sockets[$i]->[2] : $!"); 587 next; 588 } 589 setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)); 590 if ($sockets[$i]->[2] eq PF_INET()) { 591 $pack = pack_sockaddr_in($sockets[$i]->[1], $sockets[$i]->[0]); 592 } 593 else { 594 $pack = pack_sockaddr_in6($sockets[$i]->[1], $sockets[$i]->[0]); 595 setsockopt($fh, 41, 26, pack("l", 1)); # IPv6 only 596 } 597 for($j=0; $j<5; $j++) { 598 last if (bind($fh, $pack)); 599 sleep(1); 600 } 601 if ($j == 5) { 602 # All attempts failed .. give up 603 if ($sockets[$i]->[0] eq INADDR_ANY || 604 $use_ipv6 && $sockets[$i]->[0] eq in6addr_any()) { 605 push(@sockerrs, 606 "Failed to bind to port $sockets[$i]->[1] : $!"); 607 $tried_inaddr_any = 1; 608 } 609 else { 610 $ip = &network_to_address($sockets[$i]->[0]); 611 push(@sockerrs, 612 "Failed to bind to IP $ip port ". 613 "$sockets[$i]->[1] : $!"); 614 } 615 } 616 else { 617 listen($fh, &get_somaxconn()); 618 push(@socketfhs, $fh); 619 push(@listening_on_ports, $sockets[$i]->[1]); 620 $ipv6fhs{$fh} = $sockets[$i]->[2] eq PF_INET() ? 0 : 1; 621 } 622 } 623foreach $se (@sockerrs) { 624 print STDERR $se,"\n"; 625 } 626 627# If all binds failed, try binding to any address 628if (!@socketfhs && !$tried_inaddr_any) { 629 print STDERR "Falling back to listening on any address\n"; 630 $fh = "MAIN"; 631 socket($fh, PF_INET(), SOCK_STREAM, $proto) || 632 die "Failed to open socket : $!"; 633 setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)); 634 if (!bind($fh, pack_sockaddr_in($sockets[0]->[1], INADDR_ANY))) { 635 print STDERR "Failed to bind to port $sockets[0]->[1] : $!\n"; 636 exit(1); 637 } 638 listen($fh, &get_somaxconn()); 639 push(@socketfhs, $fh); 640 } 641elsif (!@socketfhs && $tried_inaddr_any) { 642 print STDERR "Could not listen on any ports"; 643 exit(1); 644 } 645 646if ($config{'listen'}) { 647 # Open the socket that allows other webmin servers to find this one 648 $proto = getprotobyname('udp'); 649 if (socket(LISTEN, PF_INET(), SOCK_DGRAM, $proto)) { 650 setsockopt(LISTEN, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)); 651 bind(LISTEN, pack_sockaddr_in($config{'listen'}, INADDR_ANY)); 652 listen(LISTEN, &get_somaxconn()); 653 } 654 else { 655 $config{'listen'} = 0; 656 } 657 } 658 659# Split from the controlling terminal, unless configured not to 660if (!$config{'nofork'} && !$nofork_argv) { 661 if (fork()) { exit; } 662 } 663eval { setsid(); }; # may not work on Windows 664 665# Close standard file handles 666open(STDIN, "</dev/null"); 667open(STDOUT, ">/dev/null"); 668&redirect_stderr_to_log(); 669&log_error("miniserv.pl started"); 670foreach $msg (@startup_msg) { 671 &log_error($msg); 672 } 673 674# write out the PID file 675&write_pid_file(); 676$miniserv_main_pid = $$; 677 678# Start the log-clearing process, if needed. This checks every minute 679# to see if the log has passed its reset time, and if so clears it 680if ($config{'logclear'}) { 681 if (!($logclearer = fork())) { 682 &close_all_sockets(); 683 close(LISTEN); 684 while(1) { 685 local $write_logtime = 0; 686 local @st = stat("$config{'logfile'}.time"); 687 if (@st) { 688 if ($st[9]+$config{'logtime'}*60*60 < time()){ 689 # need to clear log 690 $write_logtime = 1; 691 unlink($config{'logfile'}); 692 } 693 } 694 else { $write_logtime = 1; } 695 if ($write_logtime) { 696 open(LOGTIME, ">$config{'logfile'}.time"); 697 print LOGTIME time(),"\n"; 698 close(LOGTIME); 699 } 700 sleep(5*60); 701 } 702 exit; 703 } 704 push(@childpids, $logclearer); 705 } 706 707# Setup the logout time dbm if needed 708if ($config{'session'}) { 709 eval "use SDBM_File"; 710 dbmopen(%sessiondb, $config{'sessiondb'}, 0700); 711 eval "\$sessiondb{'1111111111'} = 'foo bar';"; 712 if ($@) { 713 dbmclose(%sessiondb); 714 eval "use NDBM_File"; 715 dbmopen(%sessiondb, $config{'sessiondb'}, 0700); 716 } 717 else { 718 delete($sessiondb{'1111111111'}); 719 } 720 } 721 722# Run the main loop 723$SIG{'HUP'} = 'miniserv::trigger_restart'; 724$SIG{'TERM'} = 'miniserv::term_handler'; 725$SIG{'USR1'} = 'miniserv::trigger_reload'; 726$SIG{'PIPE'} = 'IGNORE'; 727local $remove_session_count = 0; 728$need_pipes = $config{'passdelay'} || $config{'session'}; 729$cron_runs = 0; 730while(1) { 731 # Check if any webmin cron jobs are ready to run 732 &execute_ready_webmin_crons($cron_runs++); 733 734 # wait for a new connection, or a message from a child process 735 local ($i, $rmask); 736 if (@childpids <= $config{'maxconns'}) { 737 # Only accept new main socket connects when ready 738 local $s; 739 foreach $s (@socketfhs) { 740 vec($rmask, fileno($s), 1) = 1; 741 } 742 } 743 else { 744 printf STDERR "too many children (%d > %d)\n", 745 scalar(@childpids), $config{'maxconns'}; 746 } 747 if ($need_pipes) { 748 for($i=0; $i<@passin; $i++) { 749 vec($rmask, fileno($passin[$i]), 1) = 1; 750 } 751 } 752 vec($rmask, fileno(LISTEN), 1) = 1 if ($config{'listen'}); 753 754 # Wait for a connection 755 local $sel = select($rmask, undef, undef, 2); 756 757 # Check the flag files 758 if ($config{'restartflag'} && -r $config{'restartflag'}) { 759 unlink($config{'restartflag'}); 760 $need_restart = 1; 761 } 762 if ($config{'reloadflag'} && -r $config{'reloadflag'}) { 763 unlink($config{'reloadflag'}); 764 $need_reload = 1; 765 } 766 if ($config{'stopflag'} && -r $config{'stopflag'}) { 767 unlink($config{'stopflag'}); 768 $need_stop = 1; 769 } 770 771 if ($need_restart) { 772 # Got a HUP signal while in select() .. restart now 773 &restart_miniserv(); 774 } 775 if ($need_reload) { 776 # Got a USR1 signal while in select() .. re-read config 777 $need_reload = 0; 778 &reload_config_file(); 779 } 780 if ($need_stop) { 781 # Stop flag file created 782 &term_handler(); 783 } 784 local $time_now = time(); 785 786 # Clean up finished processes 787 local $pid; 788 do { $pid = waitpid(-1, WNOHANG); 789 @childpids = grep { $_ != $pid } @childpids; 790 } while($pid != 0 && $pid != -1); 791 @childpids = grep { kill(0, $_) } @childpids; 792 my %childpids = map { $_, 1 } @childpids; 793 794 # Clean up connection counts from IPs that are no longer in use 795 foreach my $ip (keys %ipconnmap) { 796 $ipconnmap{$ip} = [ grep { $childpids{$_} } @{$ipconnmap{$ip}}]; 797 } 798 foreach my $net (keys %netconnmap) { 799 $netconnmap{$net} = [ grep { $childpids{$_} } @{$netconnmap{$net}}]; 800 } 801 802 # run the unblocking procedure to check if enough time has passed to 803 # unblock hosts that never been blocked because of password failures 804 $unblocked = 0; 805 if ($config{'blockhost_failures'}) { 806 $i = 0; 807 while ($i <= $#deny) { 808 if ($blockhosttime{$deny[$i]} && 809 $config{'blockhost_time'} != 0 && 810 ($time_now - $blockhosttime{$deny[$i]}) >= 811 $config{'blockhost_time'}) { 812 # the host can be unblocked now 813 $hostfail{$deny[$i]} = 0; 814 splice(@deny, $i, 1); 815 $unblocked = 1; 816 } 817 $i++; 818 } 819 } 820 821 # Do the same for blocked users 822 if ($config{'blockuser_failures'}) { 823 $i = 0; 824 while ($i <= $#deny) { 825 if ($blockusertime{$deny[$i]} && 826 $config{'blockuser_time'} != 0 && 827 ($time_now - $blockusertime{$deny[$i]}) >= 828 $config{'blockuser_time'}) { 829 # the user can be unblocked now 830 $userfail{$deny[$i]} = 0; 831 splice(@denyusers, $i, 1); 832 $unblocked = 1; 833 } 834 $i++; 835 } 836 } 837 if ($unblocked) { 838 &write_blocked_file(); 839 } 840 841 if ($config{'session'} && (++$remove_session_count%50) == 0) { 842 # Remove sessions with more than 7 days of inactivity, 843 local $s; 844 foreach $s (keys %sessiondb) { 845 local ($user, $ltime, $lip) = 846 split(/\s+/, $sessiondb{$s}); 847 if ($ltime && $time_now - $ltime > 7*24*60*60) { 848 &run_logout_script($s, $user, undef, undef); 849 &write_logout_utmp($user, $lip); 850 if ($user =~ /^\!/ || $sessiondb{$s} eq '') { 851 # Don't log anything for logged out 852 # sessions or those with no data 853 } 854 elsif ($use_syslog && $user) { 855 syslog("info", "%s", 856 "Timeout of session for $user"); 857 } 858 elsif ($use_syslog) { 859 syslog("info", "%s", 860 "Timeout of unknown session $s ". 861 "with value $sessiondb{$s}"); 862 } 863 delete($sessiondb{$s}); 864 } 865 } 866 } 867 868 if ($use_pam && $config{'pam_conv'}) { 869 # Remove PAM sessions with more than 5 minutes of inactivity 870 local $c; 871 foreach $c (values %conversations) { 872 if ($time_now - $c->{'time'} > 5*60) { 873 &end_pam_conversation($c); 874 if ($use_syslog) { 875 syslog("info", "%s", "Timeout of PAM ". 876 "session for $c->{'user'}"); 877 } 878 } 879 } 880 } 881 882 # Don't check any sockets if there is no activity 883 next if ($sel <= 0); 884 885 # Check if any of the main sockets have received a new connection 886 local $sn = 0; 887 foreach $s (@socketfhs) { 888 if (vec($rmask, fileno($s), 1)) { 889 # got new connection 890 $acptaddr = accept(SOCK, $s); 891 print DEBUG "accept returned ",length($acptaddr),"\n"; 892 if (!$acptaddr) { next; } 893 binmode(SOCK); 894 895 # Work out IP and port of client 896 local ($peerb, $peera, $peerp) = 897 &get_address_ip($acptaddr, $ipv6fhs{$s}); 898 print DEBUG "peera=$peera peerp=$peerp\n"; 899 900 # Check the number of connections from this IP 901 $ipconnmap{$peera} ||= [ ]; 902 $ipconns = $ipconnmap{$peera}; 903 if ($config{'maxconns_per_ip'} >= 0 && 904 @$ipconns > $config{'maxconns_per_ip'}) { 905 print STDERR "Too many connections (",scalar(@$ipconns),") from IP $peera\n"; 906 close(SOCK); 907 next; 908 } 909 910 # Also check the number of connections from the network 911 ($peernet = $peera) =~ s/\.\d+$/\.0/; 912 $netconnmap{$peernet} ||= [ ]; 913 $netconns = $netconnmap{$peernet}; 914 if ($config{'maxconns_per_net'} >= 0 && 915 @$netconns > $config{'maxconns_per_net'}) { 916 print STDERR "Too many connections (",scalar(@$netconns),") from network $peernet\n"; 917 close(SOCK); 918 next; 919 } 920 921 # create pipes 922 local ($PASSINr, $PASSINw, $PASSOUTr, $PASSOUTw); 923 if ($need_pipes) { 924 ($PASSINr, $PASSINw, $PASSOUTr, $PASSOUTw) = 925 &allocate_pipes(); 926 } 927 928 # Work out the local IP 929 (undef, $locala) = &get_socket_ip(SOCK, $ipv6fhs{$s}); 930 print DEBUG "locala=$locala\n"; 931 932 # Check username of connecting user 933 $localauth_user = undef; 934 if ($config{'localauth'} && $peera eq "127.0.0.1") { 935 if (open(TCP, "/proc/net/tcp")) { 936 # Get the info direct from the kernel 937 $peerh = sprintf("%4.4X", $peerp); 938 while(<TCP>) { 939 s/^\s+//; 940 local @t = split(/[\s:]+/, $_); 941 if ($t[1] eq '0100007F' && 942 $t[2] eq $peerh) { 943 $localauth_user = 944 getpwuid($t[11]); 945 last; 946 } 947 } 948 close(TCP); 949 } 950 if (!$localauth_user) { 951 # Call lsof for the info 952 local $lsofpid = open(LSOF, 953 "$config{'localauth'} -i ". 954 "TCP\@127.0.0.1:$peerp |"); 955 while(<LSOF>) { 956 if (/^(\S+)\s+(\d+)\s+(\S+)/ && 957 $2 != $$ && $2 != $lsofpid){ 958 $localauth_user = $3; 959 } 960 } 961 close(LSOF); 962 } 963 } 964 965 # Work out the hostname for this web server 966 $host = &get_socket_name(SOCK, $ipv6fhs{$s}); 967 if (!$host) { 968 print STDERR 969 "Failed to get local socket name : $!\n"; 970 close(SOCK); 971 next; 972 } 973 $port = $sockets[$sn]->[1]; 974 975 # fork the subprocess 976 local $handpid; 977 if (!($handpid = fork())) { 978 # setup signal handlers 979 print DEBUG "in subprocess\n"; 980 $SIG{'TERM'} = 'DEFAULT'; 981 $SIG{'PIPE'} = 'DEFAULT'; 982 #$SIG{'CHLD'} = 'IGNORE'; 983 $SIG{'HUP'} = 'IGNORE'; 984 $SIG{'USR1'} = 'IGNORE'; 985 986 # Close the file handle for the session DBM 987 dbmclose(%sessiondb); 988 989 # close useless pipes 990 if ($need_pipes) { 991 &close_all_pipes(); 992 close($PASSINr); close($PASSOUTw); 993 } 994 &close_all_sockets(); 995 close(LISTEN); 996 997 # Initialize SSL for this connection 998 if ($use_ssl) { 999 $ssl_con = &ssl_connection_for_ip( 1000 SOCK, $ipv6fhs{$s}); 1001 print DEBUG "ssl_con returned $ssl_con\n"; 1002 $ssl_con || exit; 1003 } 1004 1005 print DEBUG 1006 "main: Starting handle_request loop pid=$$\n"; 1007 while(&handle_request($peera, $locala, 1008 $ipv6fhs{$s})) { 1009 # Loop until keepalive stops 1010 } 1011 print DEBUG 1012 "main: Done handle_request loop pid=$$\n"; 1013 shutdown(SOCK, 1); 1014 close(SOCK); 1015 close($PASSINw); close($PASSOUTw); 1016 exit; 1017 } 1018 push(@childpids, $handpid); 1019 push(@$ipconns, $handpid); 1020 push(@$netconns, $handpid); 1021 if ($need_pipes) { 1022 close($PASSINw); close($PASSOUTr); 1023 push(@passin, $PASSINr); 1024 push(@passout, $PASSOUTw); 1025 } 1026 close(SOCK); 1027 } 1028 $sn++; 1029 } 1030 1031 if ($config{'listen'} && vec($rmask, fileno(LISTEN), 1)) { 1032 # Got UDP packet from another webmin server 1033 local $rcvbuf; 1034 local $from = recv(LISTEN, $rcvbuf, 1024, 0); 1035 next if (!$from); 1036 local $fromip = inet_ntoa((unpack_sockaddr_in($from))[1]); 1037 local $toip = inet_ntoa((unpack_sockaddr_in( 1038 getsockname(LISTEN)))[1]); 1039 if ((!@deny || !&ip_match($fromip, $toip, @deny)) && 1040 (!@allow || &ip_match($fromip, $toip, @allow))) { 1041 local $listenhost = &get_socket_name(LISTEN, 0); 1042 send(LISTEN, "$listenhost:$config{'port'}:". 1043 ($use_ssl || $config{'inetd_ssl'} ? 1 : 0).":". 1044 ($config{'listenhost'} ? 1045 &get_system_hostname() : ""), 1046 0, $from) 1047 if ($listenhost); 1048 } 1049 } 1050 1051 # check for session, password-timeout and PAM messages from subprocesses 1052 for($i=0; $i<@passin; $i++) { 1053 if (vec($rmask, fileno($passin[$i]), 1)) { 1054 # this sub-process is asking about a password 1055 local $infd = $passin[$i]; 1056 local $outfd = $passout[$i]; 1057 local $inline = &sysread_line($infd); 1058 if ($inline) { 1059 print DEBUG "main: inline $inline"; 1060 } 1061 else { 1062 print DEBUG "main: inline EOF\n"; 1063 } 1064 1065 # Search for two-factor authentication flag 1066 # being passed, to mark the call as safe 1067 $inline =~ /^delay\s+(\S+)\s+(\S+)\s+(\d+)\s+(nolog)/; 1068 local $nolog = $4; 1069 1070 if ($inline =~ /^delay\s+(\S+)\s+(\S+)\s+(\d+)/) { 1071 # Got a delay request from a subprocess.. for 1072 # valid logins, there is no delay (to prevent 1073 # denial of service attacks), but for invalid 1074 # logins the delay increases with each failed 1075 # attempt. 1076 if ($3) { 1077 # login OK.. no delay 1078 print $outfd "0 0\n"; 1079 $wasblocked = $hostfail{$2} || 1080 $userfail{$1}; 1081 $hostfail{$2} = 0; 1082 $userfail{$1} = 0; 1083 if ($wasblocked) { 1084 &write_blocked_file(); 1085 } 1086 } 1087 else { 1088 # Login failed.. 1089 $hostfail{$2}++ if(!$nolog); 1090 $userfail{$1}++ if(!$nolog); 1091 $blocked = 0; 1092 1093 # Add the host to the block list, 1094 # if configured 1095 if ($config{'blockhost_failures'} && 1096 $hostfail{$2} >= 1097 $config{'blockhost_failures'}) { 1098 push(@deny, $2); 1099 $blockhosttime{$2} = $time_now; 1100 $blocked = 1; 1101 if ($use_syslog) { 1102 local $logtext = "Security alert: Host $2 blocked after $config{'blockhost_failures'} failed logins for user $1"; 1103 syslog("crit", "%s", 1104 $logtext); 1105 } 1106 } 1107 1108 # Add the user to the user block list, 1109 # if configured 1110 if ($config{'blockuser_failures'} && 1111 $userfail{$1} >= 1112 $config{'blockuser_failures'}) { 1113 push(@denyusers, $1); 1114 $blockusertime{$1} = $time_now; 1115 $blocked = 2; 1116 if ($use_syslog) { 1117 local $logtext = "Security alert: User $1 blocked after $config{'blockuser_failures'} failed logins"; 1118 syslog("crit", "%s", 1119 $logtext); 1120 } 1121 } 1122 1123 # Lock out the user's password, if enabled 1124 if ($config{'blocklock'} && 1125 $userfail{$1} >= 1126 $config{'blockuser_failures'}) { 1127 my $lk = &lock_user_password($1); 1128 $blocked = 2; 1129 if ($use_syslog) { 1130 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"; 1131 syslog("crit", "%s", 1132 $logtext); 1133 } 1134 } 1135 1136 # Send back a delay 1137 $dl = $userdlay{$1} - 1138 int(($time_now - $userlast{$1})/50); 1139 $dl = $dl < 0 ? 0 : $dl+1; 1140 print $outfd "$dl $blocked\n"; 1141 $userdlay{$1} = $dl; 1142 1143 # Write out blocked status file 1144 if ($blocked) { 1145 &write_blocked_file(); 1146 } 1147 } 1148 $userlast{$1} = $time_now; 1149 } 1150 elsif ($inline =~ /^verify\s+(\S+)\s+(\S+)\s+(\S+)/) { 1151 # Verifying a session ID 1152 local $session_id = $1; 1153 local $notimeout = $2; 1154 local $vip = $3; 1155 local $skey = $sessiondb{$session_id} ? 1156 $session_id : 1157 &hash_session_id($session_id); 1158 if (!defined($sessiondb{$skey})) { 1159 # Session doesn't exist 1160 print $outfd "0 0\n"; 1161 } 1162 else { 1163 local ($user, $ltime, $ip) = 1164 split(/\s+/, $sessiondb{$skey}); 1165 local $lot = &get_logout_time($user, $session_id); 1166 if ($lot && 1167 $time_now - $ltime > $lot*60 && 1168 !$notimeout) { 1169 # Session has timed out 1170 print $outfd "1 ",$time_now - $ltime,"\n"; 1171 #delete($sessiondb{$skey}); 1172 } 1173 elsif ($ip && $vip && $ip ne $vip && 1174 $config{'session_ip'}) { 1175 # Session was OK, but from the 1176 # wrong IP address 1177 print $outfd "3 $ip\n"; 1178 } 1179 elsif ($user =~ /^\!/) { 1180 # Logged out session 1181 print $outfd "0 0\n"; 1182 } 1183 else { 1184 # Session is OK 1185 print $outfd "2 $user\n"; 1186 $sessiondb{$skey} = "$user $time_now $ip"; 1187 } 1188 } 1189 } 1190 elsif ($inline =~ /^new\s+(\S+)\s+(\S+)\s+(\S+)/) { 1191 # Creating a new session 1192 local $session_id = $1; 1193 local $user = $2; 1194 local $ip = $3; 1195 $sessiondb{&hash_session_id($session_id)} = 1196 "$user $time_now $ip"; 1197 } 1198 elsif ($inline =~ /^delete\s+(\S+)/) { 1199 # Logging out a session 1200 local $session_id = $1; 1201 local $skey = $sessiondb{$session_id} ? 1202 $session_id : 1203 &hash_session_id($session_id); 1204 local ($user, $ltime, $ip) = 1205 split(/\s+/, $sessiondb{$skey}); 1206 $user =~ s/^\!//; 1207 print $outfd $user,"\n"; 1208 $sessiondb{$skey} = "!$user $ltime $ip"; 1209 } 1210 elsif ($inline =~ /^pamstart\s+(\S+)\s+(\S+)\s+(.*)/) { 1211 # Starting a new PAM conversation 1212 local ($cid, $host, $user) = ($1, $2, $3); 1213 1214 # Does this user even need PAM? 1215 local ($realuser, $canlogin) = 1216 &can_user_login($user, undef, $host); 1217 local $conv; 1218 if ($canlogin == 0) { 1219 # Cannot even login! 1220 print $outfd "0 Invalid username\n"; 1221 } 1222 elsif ($canlogin != 2) { 1223 # Not using PAM .. so just ask for 1224 # the password. 1225 $conv = { 'user' => $realuser, 1226 'host' => $host, 1227 'step' => 0, 1228 'cid' => $cid, 1229 'time' => time() }; 1230 print $outfd "3 Password\n"; 1231 } 1232 else { 1233 # Start the PAM conversation 1234 # sub-process, and get a question 1235 $conv = { 'user' => $realuser, 1236 'host' => $host, 1237 'cid' => $cid, 1238 'time' => time() }; 1239 local ($PAMINr, $PAMINw, $PAMOUTr, 1240 $PAMOUTw) = &allocate_pipes(); 1241 local $pampid = fork(); 1242 if (!$pampid) { 1243 close($PAMOUTr); close($PAMINw); 1244 &pam_conversation_process( 1245 $realuser, 1246 $PAMOUTw, $PAMINr); 1247 } 1248 close($PAMOUTw); close($PAMINr); 1249 $conv->{'pid'} = $pampid; 1250 $conv->{'PAMOUTr'} = $PAMOUTr; 1251 $conv->{'PAMINw'} = $PAMINw; 1252 push(@childpids, $pampid); 1253 1254 # Get the first PAM question 1255 local $pok = &recv_pam_question( 1256 $conv, $outfd); 1257 if (!$pok) { 1258 &end_pam_conversation($conv); 1259 } 1260 } 1261 1262 $conversations{$cid} = $conv if ($conv); 1263 } 1264 elsif ($inline =~ /^pamanswer\s+(\S+)\s+(.*)/) { 1265 # A response to a PAM question 1266 local ($cid, $answer) = ($1, $2); 1267 local $conv = $conversations{$cid}; 1268 if (!$conv) { 1269 # No such conversation? 1270 print $outfd "0 Bad login session\n"; 1271 } 1272 elsif ($conv->{'pid'}) { 1273 # Send the PAM response and get 1274 # the next question 1275 &send_pam_answer($conv, $answer); 1276 local $pok = &recv_pam_question($conv, $outfd); 1277 if (!$pok) { 1278 &end_pam_conversation($conv); 1279 } 1280 } 1281 else { 1282 # This must be the password .. try it 1283 # and send back the results 1284 local ($vu, $expired, $nonexist) = 1285 &validate_user($conv->{'user'}, 1286 $answer, 1287 $conf->{'host'}); 1288 local $ok = $vu ? 1 : 0; 1289 print $outfd "2 $conv->{'user'} $ok $expired $notexist\n"; 1290 &end_pam_conversation($conv); 1291 } 1292 } 1293 elsif ($inline =~ /^writesudo\s+(\S+)\s+(\d+)/) { 1294 # Store the fact that some user can sudo to root 1295 local ($user, $ok) = ($1, $2); 1296 $sudocache{$user} = $ok." ".time(); 1297 } 1298 elsif ($inline =~ /^readsudo\s+(\S+)/) { 1299 # Query the user sudo cache (valid for 1 minute) 1300 local $user = $1; 1301 local ($ok, $last) = 1302 split(/\s+/, $sudocache{$user}); 1303 if ($last < time()-60) { 1304 # Cache too old 1305 print $outfd "2\n"; 1306 } 1307 else { 1308 # Tell client OK or not 1309 print $outfd "$ok\n"; 1310 } 1311 } 1312 elsif ($inline =~ /\S/) { 1313 # Unknown line from pipe? 1314 print DEBUG "main: Unknown line from pipe $inline\n"; 1315 print STDERR "Unknown line from pipe $inline\n"; 1316 } 1317 else { 1318 # close pipe 1319 close($infd); close($outfd); 1320 $passin[$i] = $passout[$i] = undef; 1321 } 1322 } 1323 } 1324 @passin = grep { defined($_) } @passin; 1325 @passout = grep { defined($_) } @passout; 1326 } 1327 1328# handle_request(remoteaddress, localaddress, ipv6-flag) 1329# Where the real work is done 1330sub handle_request 1331{ 1332local ($acptip, $localip, $ipv6) = @_; 1333print DEBUG "handle_request: from $acptip to $localip ipv6=$ipv6\n"; 1334if ($config{'loghost'}) { 1335 $acpthost = &to_hostname($acptip); 1336 $acpthost = $acptip if (!$acpthost); 1337 } 1338else { 1339 $acpthost = $acptip; 1340 } 1341$loghost = $acpthost; 1342$datestr = &http_date(time()); 1343$ok_code = 200; 1344$ok_message = "Document follows"; 1345$logged_code = undef; 1346$reqline = $request_uri = $page = undef; 1347$authuser = undef; 1348$validated = undef; 1349 1350# check address against access list 1351if (@deny && &ip_match($acptip, $localip, @deny) || 1352 @allow && !&ip_match($acptip, $localip, @allow)) { 1353 &http_error(403, "Access denied for ".&html_strip($acptip)); 1354 return 0; 1355 } 1356 1357if ($use_libwrap) { 1358 # Check address with TCP-wrappers 1359 if (!hosts_ctl($config{'pam'}, STRING_UNKNOWN, 1360 $acptip, STRING_UNKNOWN)) { 1361 &http_error(403, "Access denied for ".&html_strip($acptip). 1362 " by TCP wrappers"); 1363 return 0; 1364 } 1365 } 1366print DEBUG "handle_request: passed IP checks\n"; 1367 1368# Compute a timeout for the start of headers, based on the number of 1369# child processes. As this increases, we use a shorter timeout to avoid 1370# an attacker overloading the system. 1371local $header_timeout = 60 + ($config{'maxconns'} - @childpids) * 10; 1372 1373# Wait at most 60 secs for start of headers for initial requests, or 1374# 10 minutes for kept-alive connections 1375local $rmask; 1376vec($rmask, fileno(SOCK), 1) = 1; 1377local $to = $checked_timeout ? 10*60 : $header_timeout; 1378local $sel = select($rmask, undef, undef, $to); 1379if (!$sel) { 1380 if ($checked_timeout) { 1381 print DEBUG "handle_request: exiting due to timeout of $to\n"; 1382 exit; 1383 } 1384 else { 1385 &http_error(400, "Timeout", 1386 "Waited for $to seconds for start of headers"); 1387 } 1388 } 1389$checked_timeout++; 1390print DEBUG "handle_request: passed timeout check\n"; 1391 1392# Read the HTTP request and headers 1393local $origreqline = &read_line(); 1394($reqline = $origreqline) =~ s/\r|\n//g; 1395$method = $page = $request_uri = undef; 1396print DEBUG "handle_request reqline=$reqline\n"; 1397if (!$reqline && (!$use_ssl || $checked_timeout > 1)) { 1398 # An empty request .. just close the connection 1399 print DEBUG "handle_request: rejecting empty request\n"; 1400 return 0; 1401 } 1402elsif ($reqline !~ /^(\S+)\s+(.*)\s+HTTP\/1\..$/) { 1403 print DEBUG "handle_request: invalid reqline=$reqline\n"; 1404 if ($use_ssl) { 1405 # This could be an http request when it should be https 1406 $use_ssl = 0; 1407 local $urlhost = $config{'musthost'} || $host; 1408 $urlhost = "[".$urlhost."]" if (&check_ip6address($urlhost)); 1409 local $wantport = $port; 1410 if ($wantport == 80 && 1411 &indexof(443, @listening_on_ports) >= 0) { 1412 # Connection was to port 80, but since we are also 1413 # accepting on port 443, redirect to that 1414 $wantport = 443; 1415 } 1416 local $url = $wantport == 443 ? "https://$urlhost/" 1417 : "https://$urlhost:$wantport/"; 1418 if ($config{'ssl_redirect'}) { 1419 # Just re-direct to the correct URL 1420 sleep(1); # Give browser a change to finish 1421 # sending its request 1422 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 1423 &write_data("Date: $datestr\r\n"); 1424 &write_data("Server: $config{'server'}\r\n"); 1425 &write_data("Location: $url\r\n"); 1426 &write_keep_alive(0); 1427 &write_data("\r\n"); 1428 return 0; 1429 } elsif ($config{'hide_admin_url'} != 1) { 1430 # Tell user the correct URL 1431 &http_error(200, "Document follows", 1432 "This web server is running in SSL mode. ". 1433 "Try the URL <a href='$url'>$url</a> ". 1434 "instead.", 0, 1); 1435 } else { 1436 # Throw an error 1437 &http_error(404, "Page not found", 1438 "The requested URL was not found on this server.") 1439 } 1440 } elsif (ord(substr($reqline, 0, 1)) == 128 && !$use_ssl) { 1441 # This could be an https request when it should be http .. 1442 # need to fake a HTTP response 1443 eval <<'EOF'; 1444 use Net::SSLeay; 1445 eval "Net::SSLeay::SSLeay_add_ssl_algorithms()"; 1446 eval "Net::SSLeay::load_error_strings()"; 1447 $ssl_ctx = Net::SSLeay::CTX_new(); 1448 Net::SSLeay::CTX_use_RSAPrivateKey_file( 1449 $ssl_ctx, $config{'keyfile'}, 1450 &Net::SSLeay::FILETYPE_PEM); 1451 Net::SSLeay::CTX_use_certificate_file( 1452 $ssl_ctx, 1453 $config{'certfile'} || $config{'keyfile'}, 1454 &Net::SSLeay::FILETYPE_PEM); 1455 $ssl_con = Net::SSLeay::new($ssl_ctx); 1456 pipe(SSLr, SSLw); 1457 if (!fork()) { 1458 close(SSLr); 1459 select(SSLw); $| = 1; select(STDOUT); 1460 print SSLw $origreqline; 1461 local $buf; 1462 while(sysread(SOCK, $buf, 1) > 0) { 1463 print SSLw $buf; 1464 } 1465 close(SOCK); 1466 exit; 1467 } 1468 close(SSLw); 1469 Net::SSLeay::set_wfd($ssl_con, fileno(SOCK)); 1470 Net::SSLeay::set_rfd($ssl_con, fileno(SSLr)); 1471 Net::SSLeay::accept($ssl_con) || die "accept() failed"; 1472 $use_ssl = 1; 1473 local $url = $config{'musthost'} ? 1474 "https://$config{'musthost'}:$port/" : 1475 "https://$host:$port/"; 1476 if ($config{'ssl_redirect'}) { 1477 # Just re-direct to the correct URL 1478 sleep(1); # Give browser a change to 1479 # finish sending its request 1480 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 1481 &write_data("Date: $datestr\r\n"); 1482 &write_data("Server: $config{'server'}\r\n"); 1483 &write_data("Location: $url\r\n"); 1484 &write_keep_alive(0); 1485 &write_data("\r\n"); 1486 return 0; 1487 } elsif ($config{'hide_admin_url'} != 1) { 1488 # Tell user the correct URL 1489 &http_error(200, "Bad Request", "This web server is not running in SSL mode. Try the URL <a href='$url'>$url</a> instead.", 0, 1); 1490 } else { 1491 &http_error(404, "Page not found", 1492 "The requested URL was not found on this server."); 1493 } 1494EOF 1495 if ($@) { 1496 &http_error(400, "Bad Request"); 1497 } 1498 } 1499 else { 1500 &http_error(400, "Bad Request"); 1501 } 1502 } 1503$method = $1; 1504$request_uri = $page = $2; 1505%header = (); 1506local $lastheader; 1507while(1) { 1508 ($headline = &read_line()) =~ s/\r|\n//g; 1509 last if ($headline eq ""); 1510 print DEBUG "handle_request: got headline $headline\n"; 1511 if ($headline =~ /^(\S+):\s*(.*)$/) { 1512 $header{$lastheader = lc($1)} = $2; 1513 } 1514 elsif ($headline =~ /^\s+(.*)$/) { 1515 $header{$lastheader} .= $headline; 1516 } 1517 else { 1518 &http_error(400, "Bad Header ".&html_strip($headline)); 1519 } 1520 if (&is_bad_header($header{$lastheader}, $lastheader)) { 1521 delete($header{$lastheader}); 1522 &http_error(400, "Bad Header Contents ". 1523 &html_strip($lastheader)); 1524 } 1525 } 1526 1527# If a remote IP is given in a header (such as via a proxy), only use it 1528# for logging unless trust_real_ip is set 1529local $headerhost = $header{'x-forwarded-for'} || 1530 $header{'x-real-ip'}; 1531if ($headerhost) { 1532 # Only real IPs are allowed 1533 $headerhost = undef if (!&check_ipaddress($headerhost) && 1534 !&check_ip6address($headerhost)); 1535 } 1536if ($config{'trust_real_ip'}) { 1537 $acpthost = $headerhost || $acpthost; 1538 if (&check_ipaddress($headerhost) || &check_ip6address($headerhost)) { 1539 # If a remote IP was given, use it for all access control checks 1540 # from now on. 1541 $acptip = $headerhost; 1542 1543 # re-check remote address against access list 1544 if (@deny && &ip_match($acptip, $localip, @deny) || 1545 @allow && !&ip_match($acptip, $localip, @allow)) { 1546 &http_error(403, "Access denied for ".&html_strip($acptip)); 1547 return 0; 1548 } 1549 1550 if ($use_libwrap) { 1551 # Check address with TCP-wrappers 1552 if (!hosts_ctl($config{'pam'}, STRING_UNKNOWN, 1553 $acptip, STRING_UNKNOWN)) { 1554 &http_error(403, "Access denied for ".&html_strip($acptip). 1555 " by TCP wrappers"); 1556 return 0; 1557 } 1558 } 1559 print DEBUG "handle_request: passed Remote IP checks\n"; 1560 } 1561 $loghost = $acpthost; 1562 } 1563else { 1564 $loghost = $headerhost || $loghost; 1565 } 1566 1567if (defined($header{'host'})) { 1568 if ($header{'host'} =~ /^\[(.+)\]:([0-9]+)$/) { 1569 ($host, $port) = ($1, $2); 1570 } 1571 elsif ($header{'host'} =~ /^([^:]+):([0-9]+)$/) { 1572 ($host, $port) = ($1, $2); 1573 } 1574 else { 1575 $host = $header{'host'}; 1576 } 1577 if ($config{'musthost'} && $host ne $config{'musthost'}) { 1578 # Disallowed hostname used 1579 &http_error(400, "Invalid HTTP hostname"); 1580 } 1581 } 1582 1583# Create strings for use in redirects 1584$ssl = $config{'redirect_ssl'} ne '' ? $config{'redirect_ssl'} : 1585 $use_ssl || $config{'inetd_ssl'}; 1586$redirport = $config{'redirect_port'} || $port; 1587$portstr = $redirport == 80 && !$ssl ? "" : 1588 $redirport == 443 && $ssl ? "" : ":".$redirport; 1589$redirhost = $config{'redirect_host'} || $host; 1590$hostport = &check_ip6address($redirhost) ? "[".$redirhost."]".$portstr 1591 : $redirhost.$portstr; 1592 1593# If the redirect_prefix exists change redirect base to include the prefix #1271 1594if ($config{'redirect_prefix'}) { 1595 $hostport .= $config{'redirect_prefix'} 1596 } 1597$prot = $ssl ? "https" : "http"; 1598 1599undef(%in); 1600if ($page =~ /^([^\?]+)\?(.*)$/) { 1601 # There is some query string information 1602 $page = $1; 1603 $querystring = $2; 1604 print DEBUG "handle_request: querystring=$querystring\n"; 1605 if ($querystring !~ /=/) { 1606 $queryargs = $querystring; 1607 $queryargs =~ s/\+/ /g; 1608 $queryargs =~ s/%(..)/pack("c",hex($1))/ge; 1609 $querystring = ""; 1610 } 1611 else { 1612 # Parse query-string parameters 1613 local @in = split(/\&/, $querystring); 1614 foreach $i (@in) { 1615 local ($k, $v) = split(/=/, $i, 2); 1616 $k =~ s/\+/ /g; $k =~ s/%(..)/pack("c",hex($1))/ge; 1617 $v =~ s/\+/ /g; $v =~ s/%(..)/pack("c",hex($1))/ge; 1618 $in{$k} = $v; 1619 } 1620 } 1621 } 1622$posted_data = undef; 1623if ($method eq 'POST' && 1624 $header{'content-type'} eq 'application/x-www-form-urlencoded') { 1625 # Read in posted query string information, up the configured maximum 1626 # post request length 1627 $clen = $header{"content-length"}; 1628 $clen_read = $clen > $config{'max_post'} ? $config{'max_post'} : $clen; 1629 while(length($posted_data) < $clen_read) { 1630 alarm(60); 1631 $SIG{'ALRM'} = sub { die "timeout" }; 1632 eval { 1633 $buf = &read_data($clen_read - length($posted_data)); 1634 }; 1635 alarm(0); 1636 if ($@) { 1637 &http_error(500, "Timeout reading POST request"); 1638 } 1639 if (!length($buf)) { 1640 &http_error(500, "Failed to read POST request"); 1641 } 1642 chomp($posted_data); 1643 $posted_data =~ s/\015$//mg; 1644 $posted_data .= $buf; 1645 } 1646 print DEBUG "clen_read=$clen_read clen=$clen posted_data=",length($posted_data),"\n"; 1647 if ($clen_read != $clen && length($posted_data) > $clen) { 1648 # If the client sent more data than we asked for, chop the 1649 # rest off 1650 $posted_data = substr($posted_data, 0, $clen); 1651 } 1652 if (length($posted_data) > $clen) { 1653 # When the client sent too much, delay so that it gets headers 1654 sleep(3); 1655 } 1656 if ($header{'user-agent'} =~ /MSIE/ && 1657 $header{'user-agent'} !~ /Opera/i) { 1658 # MSIE includes an extra newline in the data 1659 $posted_data =~ s/\r|\n//g; 1660 } 1661 local @in = split(/\&/, $posted_data); 1662 foreach $i (@in) { 1663 local ($k, $v) = split(/=/, $i, 2); 1664 #$v =~ s/\r|\n//g; 1665 $k =~ s/\+/ /g; $k =~ s/%(..)/pack("c",hex($1))/ge; 1666 $v =~ s/\+/ /g; $v =~ s/%(..)/pack("c",hex($1))/ge; 1667 $in{$k} = $v; 1668 } 1669 print DEBUG "handle_request: posted_data=$posted_data\n"; 1670 } 1671 1672# Reject CONNECT request, which isn't supported 1673if ($method eq "CONNECT" || $method eq "TRACE") { 1674 &http_error(405, "Method ".&html_strip($method)." is not supported"); 1675 } 1676 1677# work out accepted encodings 1678%acceptenc = map { $_, 1 } split(/,/, $header{'accept-encoding'}); 1679 1680# replace %XX sequences in page 1681$page =~ s/%(..)/pack("c",hex($1))/ge; 1682 1683# Check if the browser's user agent indicates a mobile device 1684$mobile_device = &is_mobile_useragent($header{'user-agent'}); 1685 1686# Check if Host: header is for a mobile URL 1687foreach my $m (@mobile_prefixes) { 1688 if ($header{'host'} =~ /^\Q$m\E/i) { 1689 $mobile_device = 1; 1690 } 1691 } 1692 1693# check for the logout flag file, and if existent deny authentication 1694if ($config{'logout'} && -r $config{'logout'}.$in{'miniserv_logout_id'}) { 1695 print DEBUG "handle_request: logout flag set\n"; 1696 $deny_authentication++; 1697 open(LOGOUT, $config{'logout'}.$in{'miniserv_logout_id'}); 1698 chop($count = <LOGOUT>); 1699 close(LOGOUT); 1700 $count--; 1701 if ($count > 0) { 1702 open(LOGOUT, ">$config{'logout'}$in{'miniserv_logout_id'}"); 1703 print LOGOUT "$count\n"; 1704 close(LOGOUT); 1705 } 1706 else { 1707 unlink($config{'logout'}.$in{'miniserv_logout_id'}); 1708 } 1709 } 1710 1711# check for any redirect for the requested URL 1712foreach my $pfx (@strip_prefix) { 1713 my $l = length($pfx); 1714 if(length($page) >= $l && 1715 substr($page,0,$l) eq $pfx) { 1716 $page=substr($page,$l); 1717 last; 1718 } 1719 } 1720$simple = &simplify_path($page, $bogus); 1721$rpath = $simple; 1722$rpath .= "&".$querystring if (defined($querystring)); 1723$redir = $redirect{$rpath}; 1724if (defined($redir)) { 1725 print DEBUG "handle_request: redir=$redir\n"; 1726 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 1727 &write_data("Date: $datestr\r\n"); 1728 &write_data("Server: $config{'server'}\r\n"); 1729 &write_data("Location: $prot://$hostport$redir\r\n"); 1730 &write_keep_alive(0); 1731 &write_data("\r\n"); 1732 return 0; 1733 } 1734 1735# Check for a DAV request 1736$davpath = undef; 1737foreach my $d (@davpaths) { 1738 if ($simple eq $d || $simple =~ /^\Q$d\E\//) { 1739 $davpath = $d; 1740 last; 1741 } 1742 } 1743if (!$davpath && ($method eq "SEARCH" || $method eq "PUT")) { 1744 &http_error(400, "Bad Request method ".&html_strip($method)); 1745 } 1746 1747# Check for password if needed 1748if ($config{'userfile'}) { 1749 print DEBUG "handle_request: Need authentication\n"; 1750 $validated = 0; 1751 $blocked = 0; 1752 1753 # Session authentication is never used for connections by 1754 # another webmin server, or for specified pages, or for DAV, or XMLRPC, 1755 # or mobile browsers if requested. 1756 if ($header{'user-agent'} =~ /webmin/i || 1757 $header{'user-agent'} =~ /$config{'agents_nosession'}/i || 1758 $sessiononly{$simple} || $davpath || 1759 $simple eq "/xmlrpc.cgi" || 1760 $acptip eq $config{'host_nosession'} || 1761 $mobile_device && $config{'mobile_nosession'}) { 1762 print DEBUG "handle_request: Forcing HTTP authentication\n"; 1763 $config{'session'} = 0; 1764 } 1765 1766 # Check for SSL authentication 1767 if ($use_ssl && $verified_client) { 1768 $peername = Net::SSLeay::X509_NAME_oneline( 1769 Net::SSLeay::X509_get_subject_name( 1770 Net::SSLeay::get_peer_certificate( 1771 $ssl_con))); 1772 $u = &find_user_by_cert($peername); 1773 if ($u) { 1774 $authuser = $u; 1775 $validated = 2; 1776 } 1777 if ($use_syslog && !$validated) { 1778 syslog("crit", "%s", 1779 "Unknown SSL certificate $peername"); 1780 } 1781 } 1782 1783 if (!$validated && !$deny_authentication) { 1784 # check for IP-based authentication 1785 local $a; 1786 foreach $a (keys %ipaccess) { 1787 if ($acptip eq $a) { 1788 # It does! Auth as the user 1789 $validated = 3; 1790 $baseauthuser = $authuser = 1791 $ipaccess{$a}; 1792 } 1793 } 1794 } 1795 1796 # Check for normal HTTP authentication 1797 if (!$validated && !$deny_authentication && !$config{'session'} && 1798 $header{authorization} =~ /^basic\s+(\S+)$/i) { 1799 # authorization given.. 1800 ($authuser, $authpass) = split(/:/, &b64decode($1), 2); 1801 print DEBUG "handle_request: doing basic auth check authuser=$authuser authpass=$authpass\n"; 1802 local ($vu, $expired, $nonexist, $wvu) = 1803 &validate_user($authuser, $authpass, $host, 1804 $acptip, $port); 1805 print DEBUG "handle_request: vu=$vu expired=$expired nonexist=$nonexist\n"; 1806 if ($vu && (!$expired || $config{'passwd_mode'} == 1)) { 1807 $authuser = $vu; 1808 $validated = 1; 1809 } 1810 else { 1811 $validated = 0; 1812 } 1813 if ($use_syslog && !$validated) { 1814 syslog("crit", "%s", 1815 ($nonexist ? "Non-existent" : 1816 $expired ? "Expired" : "Invalid"). 1817 " login as $authuser from $acpthost"); 1818 } 1819 if ($authuser =~ /\r|\n|\s/) { 1820 &http_error(500, "Invalid username", 1821 "Username contains invalid characters"); 1822 } 1823 if ($authpass =~ /\r|\n/) { 1824 &http_error(500, "Invalid password", 1825 "Password contains invalid characters"); 1826 } 1827 1828 if ($config{'passdelay'} && !$config{'inetd'} && $authuser) { 1829 # check with main process for delay 1830 print DEBUG "handle_request: about to ask for password delay\n"; 1831 print $PASSINw "delay $authuser $acptip $validated\n"; 1832 <$PASSOUTr> =~ /(\d+) (\d+)/; 1833 $blocked = $2; 1834 print DEBUG "handle_request: password delay $1 $2\n"; 1835 sleep($1); 1836 } 1837 } 1838 1839 # Check for a visit to the special session login page 1840 if ($config{'session'} && !$deny_authentication && 1841 $page eq $config{'session_login'}) { 1842 if ($in{'logout'} && $header{'cookie'} =~ /(^|\s|;)$sidname=([a-f0-9]+)/) { 1843 # Logout clicked .. remove the session 1844 local $sid = $2; 1845 print $PASSINw "delete $sid\n"; 1846 local $louser = <$PASSOUTr>; 1847 chop($louser); 1848 $logout = 1; 1849 $already_session_id = undef; 1850 $authuser = $baseauthuser = undef; 1851 if ($louser) { 1852 if ($use_syslog) { 1853 syslog("info", "%s", "Logout by $louser from $acpthost"); 1854 } 1855 &run_logout_script($louser, $sid, 1856 $loghost, $localip); 1857 &write_logout_utmp($louser, $actphost); 1858 } 1859 } 1860 elsif ($in{'session'}) { 1861 # Session ID given .. put it in the cookie if valid 1862 local $sid = $in{'session'}; 1863 if ($sid =~ /\r|\n|\s/) { 1864 &http_error(500, "Invalid session", 1865 "Session ID contains invalid characters"); 1866 } 1867 print $PASSINw "verify $sid 0 $acptip\n"; 1868 <$PASSOUTr> =~ /(\d+)\s+(\S+)/; 1869 if ($1 != 2) { 1870 &http_error(500, "Invalid session", 1871 "Session ID is not valid"); 1872 } 1873 local $vu = $2; 1874 local $hrv = &handle_login( 1875 $vu, $vu ? 1 : 0, 1876 0, 0, undef, 1, 0); 1877 return $hrv if (defined($hrv)); 1878 } 1879 else { 1880 # Trim username to remove leading and trailing spaces to 1881 # be able to login, if username pastes from somewhere 1882 $in{'user'} =~ s/^\s+|\s+$//g; 1883 1884 # Validate the user 1885 if ($in{'user'} =~ /\r|\n|\s/) { 1886 &run_failed_script($in{'user'}, 'baduser', 1887 $loghost, $localip); 1888 &http_error(500, "Invalid username", 1889 "Username contains invalid characters"); 1890 } 1891 if ($in{'pass'} =~ /\r|\n/) { 1892 &run_failed_script($in{'user'}, 'badpass', 1893 $loghost, $localip); 1894 &http_error(500, "Invalid password", 1895 "Password contains invalid characters"); 1896 } 1897 1898 local ($vu, $expired, $nonexist, $wvu) = 1899 &validate_user($in{'user'}, $in{'pass'}, $host, 1900 $acptip, $port); 1901 if ($vu && $wvu) { 1902 my $uinfo = &get_user_details($wvu, $vu); 1903 if ($uinfo && $uinfo->{'twofactor_provider'}) { 1904 # Check two-factor token ID 1905 $err = &validate_twofactor( 1906 $wvu, $in{'twofactor'}, $vu); 1907 if ($err) { 1908 &run_failed_script( 1909 $vu, 'twofactor', 1910 $loghost, $localip); 1911 $twofactor_msg = $err; 1912 $twofactor_nolog = 'nolog' if (!$in{'twofactor'}); 1913 $vu = undef; 1914 } 1915 } 1916 } 1917 local $hrv = &handle_login( 1918 $vu || $in{'user'}, $vu ? 1 : 0, 1919 $expired, $nonexist, $in{'pass'}, 1920 $in{'notestingcookie'}, $twofactor_nolog); 1921 return $hrv if (defined($hrv)); 1922 } 1923 } 1924 1925 # Check for a visit to the special PAM login page 1926 if ($config{'session'} && !$deny_authentication && 1927 $use_pam && $config{'pam_conv'} && $page eq $config{'pam_login'} && 1928 !$in{'restart'}) { 1929 # A question has been entered .. submit it to the main process 1930 print DEBUG "handle_request: Got call to $page ($in{'cid'})\n"; 1931 print DEBUG "handle_request: For PAM, authuser=$authuser\n"; 1932 if ($in{'answer'} =~ /\r|\n/ || $in{'cid'} =~ /\r|\n|\s/) { 1933 &http_error(500, "Invalid response", 1934 "Response contains invalid characters"); 1935 } 1936 1937 if (!$in{'cid'}) { 1938 # Start of a new conversation - answer must be username 1939 $cid = &generate_random_id(); 1940 print $PASSINw "pamstart $cid $host $in{'answer'}\n"; 1941 } 1942 else { 1943 # A response to a previous question 1944 $cid = $in{'cid'}; 1945 print $PASSINw "pamanswer $cid $in{'answer'}\n"; 1946 } 1947 1948 # Read back the response, and the next question (if any) 1949 local $line = <$PASSOUTr>; 1950 $line =~ s/\r|\n//g; 1951 local ($rv, $question) = split(/\s+/, $line, 2); 1952 if ($rv == 0) { 1953 # Cannot login! 1954 local $hrv = &handle_login( 1955 !$in{'cid'} && $in{'answer'} ? $in{'answer'} 1956 : "unknown", 1957 0, 0, 1, undef); 1958 return $hrv if (defined($hrv)); 1959 } 1960 elsif ($rv == 1 || $rv == 3) { 1961 # Another question .. force use of PAM CGI 1962 $validated = 1; 1963 $method = "GET"; 1964 $querystring .= "&cid=$cid&question=". 1965 &urlize($question); 1966 $querystring .= "&password=1" if ($rv == 3); 1967 $queryargs = ""; 1968 $page = $config{'pam_login'}; 1969 $miniserv_internal = 1; 1970 $logged_code = 401; 1971 } 1972 elsif ($rv == 2) { 1973 # Got back a final ok or failure 1974 local ($user, $ok, $expired, $nonexist) = 1975 split(/\s+/, $question); 1976 local $hrv = &handle_login( 1977 $user, $ok, $expired, $nonexist, undef, 1978 $in{'notestingcookie'}); 1979 return $hrv if (defined($hrv)); 1980 } 1981 elsif ($rv == 4) { 1982 # A message from PAM .. tell the user 1983 $validated = 1; 1984 $method = "GET"; 1985 $querystring .= "&cid=$cid&message=". 1986 &urlize($question); 1987 $queryargs = ""; 1988 $page = $config{'pam_login'}; 1989 $miniserv_internal = 1; 1990 $logged_code = 401; 1991 } 1992 } 1993 1994 # Check for a visit to the special password change page 1995 if ($config{'session'} && !$deny_authentication && 1996 $page eq $config{'password_change'} && !$validated) { 1997 # Just let this slide .. 1998 $validated = 1; 1999 $miniserv_internal = 3; 2000 } 2001 2002 # Check for an existing session 2003 if ($config{'session'} && !$validated) { 2004 if ($already_session_id) { 2005 $session_id = $already_session_id; 2006 $authuser = $already_authuser; 2007 $validated = 1; 2008 } 2009 elsif (!$deny_authentication && 2010 $header{'cookie'} =~ /(^|\s|;)$sidname=([a-f0-9]+)/) { 2011 # Try all session cookies 2012 local $cookie = $header{'cookie'}; 2013 while($cookie =~ s/(^|\s|;)$sidname=([a-f0-9]+)//) { 2014 $session_id = $2; 2015 local $notimeout = 2016 $in{'webmin_notimeout'} ? 1 : 0; 2017 print $PASSINw "verify $session_id $notimeout $acptip\n"; 2018 <$PASSOUTr> =~ /(\d+)\s+(\S+)/; 2019 if ($1 == 2) { 2020 # Valid session continuation 2021 $validated = 1; 2022 $authuser = $2; 2023 $already_authuser = $authuser; 2024 $timed_out = undef; 2025 last; 2026 } 2027 elsif ($1 == 1) { 2028 # Session timed out 2029 $timed_out = $2; 2030 } 2031 elsif ($1 == 3) { 2032 # Session is OK, but from the wrong IP 2033 print STDERR "Session $session_id was ", 2034 "used from $acptip instead of ", 2035 "original IP $2\n"; 2036 } 2037 else { 2038 # Invalid session ID .. don't set 2039 # verified flag 2040 } 2041 } 2042 } 2043 if ($authuser) { 2044 # We got a session .. but does the user still exist? 2045 my @can = &can_user_login($authuser, undef, $host); 2046 $baseauthuser = $can[3] || $authuser; 2047 my $auser = &get_user_details($baseauthuser, $authuser); 2048 if (!$auser) { 2049 print STDERR "Session $session_id is for user ", 2050 "$authuser who does not exist\n"; 2051 $validated = 0; 2052 $already_authuser = $authuser = undef; 2053 } 2054 } 2055 } 2056 2057 # Check for local authentication 2058 if ($localauth_user && !$header{'x-forwarded-for'} && !$header{'via'}) { 2059 my $luser = &get_user_details($localauth_user); 2060 if ($luser) { 2061 # Local user exists in webmin users file 2062 $validated = 1; 2063 $authuser = $localauth_user; 2064 } 2065 else { 2066 # Check if local user is allowed by unixauth 2067 local @can = &can_user_login($localauth_user, 2068 undef, $host); 2069 if ($can[0]) { 2070 $validated = 2; 2071 $authuser = $localauth_user; 2072 } 2073 else { 2074 $localauth_user = undef; 2075 } 2076 } 2077 } 2078 2079 if (!$validated) { 2080 # Check if this path allows anonymous access 2081 local $a; 2082 foreach $a (keys %anonymous) { 2083 if (substr($simple, 0, length($a)) eq $a) { 2084 # It does! Auth as the user, if IP access 2085 # control allows him. 2086 if (&check_user_ip($anonymous{$a}) && 2087 &check_user_time($anonymous{$a})) { 2088 $validated = 3; 2089 $baseauthuser = $authuser = 2090 $anonymous{$a}; 2091 } 2092 } 2093 } 2094 } 2095 2096 if (!$validated) { 2097 # Check if this path allows unauthenticated access 2098 local ($u, $unauth); 2099 foreach $u (@unauth) { 2100 $unauth++ if ($simple =~ /$u/); 2101 } 2102 if (!$bogus && $unauth) { 2103 # Unauthenticated directory or file request - approve it 2104 $validated = 4; 2105 $baseauthuser = $authuser = undef; 2106 } 2107 } 2108 2109 if (!$validated) { 2110 if ($blocked == 0) { 2111 # No password given.. ask 2112 if ($config{'pam_conv'} && $use_pam) { 2113 # Force CGI for PAM question, starting with 2114 # the username which is always needed 2115 $validated = 1; 2116 $method = "GET"; 2117 $querystring .= "&initial=1&question=". 2118 &urlize("Username"); 2119 $querystring .= "&failed=$failed_user" if ($failed_user); 2120 $querystring .= "&timed_out=$timed_out" if ($timed_out); 2121 $queryargs = ""; 2122 $page = $config{'pam_login'}; 2123 $miniserv_internal = 1; 2124 $logged_code = 401; 2125 } 2126 elsif ($config{'session'}) { 2127 # Force CGI for session login 2128 $validated = 1; 2129 if ($logout) { 2130 $querystring .= "&logout=1&page=/"; 2131 } 2132 else { 2133 # Re-direct to current module only 2134 local $rpage = $request_uri; 2135 if (!$config{'loginkeeppage'}) { 2136 $rpage =~ s/\?.*$//; 2137 $rpage =~ s/[^\/]+$// 2138 } 2139 $querystring = "page=".&urlize($rpage); 2140 } 2141 $method = "GET"; 2142 $querystring .= "&failed=".&urlize($failed_user) 2143 if ($failed_user); 2144 if ($twofactor_msg) { 2145 $querystring .= "&failed_save=".&urlize($failed_save); 2146 $querystring .= "&failed_pass=".&urlize($failed_pass); 2147 $querystring .= "&failed_twofactor_attempt=".&urlize($failed_twofactor_attempt); 2148 $querystring .= "&twofactor_msg=".&urlize($twofactor_msg); 2149 } 2150 $querystring .= "&timed_out=$timed_out" 2151 if ($timed_out); 2152 $queryargs = ""; 2153 $page = $config{'session_login'}; 2154 $miniserv_internal = 1; 2155 $logged_code = 401; 2156 } 2157 else { 2158 # Ask for login with HTTP authentication 2159 &write_data("HTTP/1.0 401 Unauthorized\r\n"); 2160 &write_data("Date: $datestr\r\n"); 2161 &write_data("Server: $config{'server'}\r\n"); 2162 &write_data("WWW-authenticate: Basic ". 2163 "realm=\"$config{'realm'}\"\r\n"); 2164 &write_keep_alive(0); 2165 &write_data("Content-type: text/html; Charset=utf-8\r\n"); 2166 &write_data("\r\n"); 2167 &reset_byte_count(); 2168 &write_data("<html>\n"); 2169 &write_data("<head>".&embed_error_styles($roots[0])."<title>401 — Unauthorized</title></head>\n"); 2170 &write_data("<body><h2 class=\"err-head\">401 — Unauthorized</h2>\n"); 2171 &write_data("<p class=\"err-content\">A password is required to access this\n"); 2172 &write_data("web server. Please try again.</p> <p>\n"); 2173 &write_data("</body></html>\n"); 2174 &log_request($loghost, undef, $reqline, 401, &byte_count()); 2175 return 0; 2176 } 2177 } 2178 elsif ($blocked == 1) { 2179 # when the host has been blocked, give it an error 2180 &http_error(403, "Access denied for $acptip. The host ". 2181 "has been blocked because of too ". 2182 "many authentication failures."); 2183 } 2184 elsif ($blocked == 2) { 2185 # when the user has been blocked, give it an error 2186 &http_error(403, "Access denied. The user ". 2187 "has been blocked because of too ". 2188 "many authentication failures."); 2189 } 2190 } 2191 else { 2192 # Get the real Webmin username 2193 if (!$baseauthuser) { 2194 local @can = &can_user_login($authuser, undef, $host); 2195 $baseauthuser = $can[3] || $authuser; 2196 } 2197 2198 if ($config{'remoteuser'} && !$< && $validated) { 2199 # Switch to the UID of the remote user (if he exists) 2200 local @u = getpwnam($authuser); 2201 if (@u && $< != $u[2]) { 2202 $( = $u[3]; $) = "$u[3] $u[3]"; 2203 ($>, $<) = ($u[2], $u[2]); 2204 } 2205 else { 2206 &http_error(500, "Unix user ". 2207 &html_strip($authuser)." does not exist"); 2208 return 0; 2209 } 2210 } 2211 } 2212 2213 # Check per-user IP access control 2214 if (!&check_user_ip($baseauthuser)) { 2215 &http_error(403, "Access denied for $acptip for ". 2216 &html_strip($baseauthuser)); 2217 return 0; 2218 } 2219 2220 # Check per-user allowed times 2221 if (!&check_user_time($baseauthuser)) { 2222 &http_error(403, "Access denied at the current time"); 2223 return 0; 2224 } 2225 } 2226$uinfo = &get_user_details($baseauthuser, $authuser); 2227 2228# Validate the path, and convert to canonical form 2229rerun: 2230$simple = &simplify_path($page, $bogus); 2231print DEBUG "handle_request: page=$page simple=$simple\n"; 2232if ($bogus) { 2233 &http_error(400, "Invalid path"); 2234 } 2235 2236# Check for a DAV request 2237if ($davpath) { 2238 return &handle_dav_request($davpath); 2239 } 2240 2241# Work out the active theme(s) 2242local $preroots = $mobile_device && defined($config{'mobile_preroot'}) ? 2243 $config{'mobile_preroot'} : 2244 $authuser && defined($config{'preroot_'.$authuser}) ? 2245 $config{'preroot_'.$authuser} : 2246 $uinfo && defined($uinfo->{'preroot'}) ? 2247 $uinfo->{'preroot'} : 2248 $config{'preroot'}; 2249local @preroots = reverse(split(/\s+/, $preroots)); 2250 2251# Canonicalize the directories 2252local @themes; 2253foreach my $preroot (@preroots) { 2254 # Always under the current webmin root 2255 $preroot =~ s/^.*\///g; 2256 push(@themes, $preroot); 2257 $preroot = $roots[0].'/'.$preroot; 2258 } 2259 2260# Look in the theme root directories first 2261local ($full, @stfull); 2262$foundroot = undef; 2263foreach my $preroot (@preroots) { 2264 $is_directory = 1; 2265 $sofar = ""; 2266 $full = $preroot.$sofar; 2267 $scriptname = $simple; 2268 foreach $b (split(/\//, $simple)) { 2269 if ($b ne "") { $sofar .= "/$b"; } 2270 $full = $preroot.$sofar; 2271 @stfull = stat($full); 2272 if (!@stfull) { undef($full); last; } 2273 2274 # Check if this is a directory 2275 if (-d _) { 2276 # It is.. go on parsing 2277 $is_directory = 1; 2278 next; 2279 } 2280 else { 2281 $is_directory = 0; 2282 } 2283 2284 # Check if this is a CGI program 2285 if (&get_type($full) eq "internal/cgi") { 2286 $pathinfo = substr($simple, length($sofar)); 2287 $pathinfo .= "/" if ($page =~ /\/$/); 2288 $scriptname = $sofar; 2289 last; 2290 } 2291 } 2292 2293 # Don't stop at a directory unless this is the last theme, which 2294 # is the 'real' one that provides the .cgi scripts 2295 if ($is_directory && $preroot ne $preroots[$#preroots]) { 2296 next; 2297 } 2298 2299 if ($full) { 2300 # Found it! 2301 if ($sofar eq '') { 2302 $cgi_pwd = $roots[0]; 2303 } 2304 elsif ($is_directory) { 2305 $cgi_pwd = "$roots[0]$sofar"; 2306 } 2307 else { 2308 "$roots[0]$sofar" =~ /^(.*\/)[^\/]+$/; 2309 $cgi_pwd = $1; 2310 } 2311 $foundroot = $preroot; 2312 if ($is_directory) { 2313 # Check for index files in the directory 2314 local $foundidx; 2315 foreach $idx (split(/\s+/, $config{"index_docs"})) { 2316 $idxfull = "$full/$idx"; 2317 local @stidxfull = stat($idxfull); 2318 if (-r _ && !-d _) { 2319 $full = $idxfull; 2320 @stfull = @stidxfull; 2321 $is_directory = 0; 2322 $scriptname .= "/" 2323 if ($scriptname ne "/"); 2324 $foundidx++; 2325 last; 2326 } 2327 } 2328 @stfull = stat($full) if (!$foundidx); 2329 } 2330 } 2331 last if ($foundroot); 2332 } 2333print DEBUG "handle_request: initial full=$full\n"; 2334 2335# Look in the real root directories, stopping when we find a file or directory 2336if (!$full || $is_directory) { 2337 ROOT: foreach $root (@roots) { 2338 $sofar = ""; 2339 $full = $root.$sofar; 2340 $scriptname = $simple; 2341 foreach $b ($simple eq "/" ? ( "" ) : split(/\//, $simple)) { 2342 if ($b ne "") { $sofar .= "/$b"; } 2343 $full = $root.$sofar; 2344 @stfull = stat($full); 2345 if (!@stfull) { 2346 next ROOT; 2347 } 2348 2349 # Check if this is a directory 2350 if (-d _) { 2351 # It is.. go on parsing 2352 next; 2353 } 2354 2355 # Check if this is a CGI program 2356 if (&get_type($full) eq "internal/cgi") { 2357 $pathinfo = substr($simple, length($sofar)); 2358 $pathinfo .= "/" if ($page =~ /\/$/); 2359 $scriptname = $sofar; 2360 last; 2361 } 2362 } 2363 2364 # Run CGI in the same directory as whatever file 2365 # was requested 2366 $full =~ /^(.*\/)[^\/]+$/; $cgi_pwd = $1; 2367 2368 if (-e $full) { 2369 # Found something! 2370 $realroot = $root; 2371 $foundroot = $root; 2372 last; 2373 } 2374 } 2375 if (!@stfull) { &http_error(404, "File not found"); } 2376 } 2377print DEBUG "handle_request: full=$full\n"; 2378@stfull = stat($full) if (!@stfull); 2379 2380# check filename against denyfile regexp 2381local $denyfile = $config{'denyfile'}; 2382if ($denyfile && $full =~ /$denyfile/) { 2383 &http_error(403, "Access denied to ".&html_strip($page)); 2384 return 0; 2385 } 2386 2387# Reached the end of the path OK.. see what we've got 2388if (-d _) { 2389 # See if the URL ends with a / as it should 2390 print DEBUG "handle_request: found a directory\n"; 2391 if ($page !~ /\/$/) { 2392 # It doesn't.. redirect 2393 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 2394 &write_data("Date: $datestr\r\n"); 2395 &write_data("Server: $config{server}\r\n"); 2396 &write_data("Location: $prot://$hostport$page/\r\n"); 2397 &write_keep_alive(0); 2398 &write_data("\r\n"); 2399 &log_request($loghost, $authuser, $reqline, 302, 0); 2400 return 0; 2401 } 2402 # A directory.. check for index files 2403 local $foundidx; 2404 foreach $idx (split(/\s+/, $config{"index_docs"})) { 2405 $idxfull = "$full/$idx"; 2406 @stidxfull = stat($idxfull); 2407 if (-r _ && !-d _) { 2408 $cgi_pwd = $full; 2409 $full = $idxfull; 2410 @stfull = @stidxfull; 2411 $scriptname .= "/" if ($scriptname ne "/"); 2412 $foundidx++; 2413 last; 2414 } 2415 } 2416 @stfull = stat($full) if (!$foundidx); 2417 } 2418if (-d _) { 2419 # This is definitely a directory.. list it 2420 if ($config{'nolistdir'}) { 2421 &http_error(500, "Directory is missing an index file"); 2422 } 2423 print DEBUG "handle_request: listing directory\n"; 2424 local $resp = "HTTP/1.0 $ok_code $ok_message\r\n". 2425 "Date: $datestr\r\n". 2426 "Server: $config{server}\r\n". 2427 "Content-type: text/html; Charset=utf-8\r\n"; 2428 &write_data($resp); 2429 &write_keep_alive(0); 2430 &write_data("\r\n"); 2431 &reset_byte_count(); 2432 &write_data("".&embed_error_styles($roots[0])."<h2 class=\"err-head\">Index of $simple</h2>\n"); 2433 &write_data("<pre class=\"err-content\">\n"); 2434 &write_data(sprintf "%-35.35s %-20.20s %-10.10s\n", 2435 "Name", "Last Modified", "Size"); 2436 &write_data("</pre>\n"); 2437 &write_data("<hr>\n"); 2438 opendir(DIR, $full); 2439 while($df = readdir(DIR)) { 2440 if ($df =~ /^\./) { next; } 2441 $fulldf = $full eq "/" ? $full.$df : $full."/".$df; 2442 (@stbuf = stat($fulldf)) || next; 2443 if (-d _) { $df .= "/"; } 2444 @tm = localtime($stbuf[9]); 2445 $fdate = sprintf "%2.2d/%2.2d/%4.4d %2.2d:%2.2d:%2.2d", 2446 $tm[3],$tm[4]+1,$tm[5]+1900, 2447 $tm[0],$tm[1],$tm[2]; 2448 $len = length($df); $rest = " "x(35-$len); 2449 &write_data(sprintf 2450 "<a href=\"%s\">%-${len}.${len}s</a>$rest %-20.20s %-10.10s\n", 2451 &urlize($df), &html_strip($df), $fdate, $stbuf[7]); 2452 } 2453 closedir(DIR); 2454 &log_request($loghost, $authuser, $reqline, $ok_code, &byte_count()); 2455 return 0; 2456 } 2457 2458# CGI or normal file 2459local $rv; 2460if (&get_type($full) eq "internal/cgi" && $validated != 4) { 2461 # A CGI program to execute 2462 print DEBUG "handle_request: executing CGI\n"; 2463 $envtz = $ENV{"TZ"}; 2464 $envuser = $ENV{"USER"}; 2465 $envpath = $ENV{"PATH"}; 2466 $envlang = $ENV{"LANG"}; 2467 $envroot = $ENV{"SystemRoot"}; 2468 $envperllib = $ENV{'PERLLIB'}; 2469 foreach my $k (keys %ENV) { 2470 delete($ENV{$k}); 2471 } 2472 $ENV{"PATH"} = $envpath if ($envpath); 2473 $ENV{"TZ"} = $envtz if ($envtz); 2474 $ENV{"USER"} = $envuser if ($envuser); 2475 $ENV{"OLD_LANG"} = $envlang if ($envlang); 2476 $ENV{"SystemRoot"} = $envroot if ($envroot); 2477 $ENV{'PERLLIB'} = $envperllib if ($envperllib); 2478 $ENV{"HOME"} = $user_homedir; 2479 $ENV{"SERVER_SOFTWARE"} = $config{"server"}; 2480 $ENV{"SERVER_NAME"} = $host; 2481 $ENV{"SERVER_ADMIN"} = $config{"email"}; 2482 $ENV{"SERVER_ROOT"} = $roots[0]; 2483 $ENV{"SERVER_REALROOT"} = $realroot; 2484 $ENV{"SERVER_PORT"} = $port; 2485 $ENV{"REMOTE_HOST"} = $acpthost; 2486 $ENV{"REMOTE_ADDR"} = $acptip; 2487 $ENV{"REMOTE_ADDR_PROTOCOL"} = $ipv6 ? 6 : 4; 2488 $ENV{"REMOTE_USER"} = $authuser; 2489 $ENV{"BASE_REMOTE_USER"} = $authuser ne $baseauthuser ? 2490 $baseauthuser : undef; 2491 $ENV{"REMOTE_PASS"} = $authpass if (defined($authpass) && 2492 $config{'pass_password'}); 2493 if ($uinfo && $uinfo->{'proto'}) { 2494 $ENV{"REMOTE_USER_PROTO"} = $uinfo->{'proto'}; 2495 $ENV{"REMOTE_USER_ID"} = $uinfo->{'id'}; 2496 } 2497 print DEBUG "REMOTE_USER = ",$ENV{"REMOTE_USER"},"\n"; 2498 print DEBUG "BASE_REMOTE_USER = ",$ENV{"BASE_REMOTE_USER"},"\n"; 2499 print DEBUG "proto=$uinfo->{'proto'} id=$uinfo->{'id'}\n" if ($uinfo); 2500 $ENV{"SSL_USER"} = $peername if ($validated == 2); 2501 $ENV{"ANONYMOUS_USER"} = "1" if ($validated == 3 || $validated == 4); 2502 $ENV{"DOCUMENT_ROOT"} = $roots[0]; 2503 $ENV{"THEME_ROOT"} = $preroots[0]; 2504 $ENV{"THEME_DIRS"} = join(" ", @themes) || ""; 2505 $ENV{"DOCUMENT_REALROOT"} = $realroot; 2506 $ENV{"GATEWAY_INTERFACE"} = "CGI/1.1"; 2507 $ENV{"SERVER_PROTOCOL"} = "HTTP/1.0"; 2508 $ENV{"REQUEST_METHOD"} = $method; 2509 $ENV{"SCRIPT_NAME"} = $scriptname; 2510 $ENV{"SCRIPT_FILENAME"} = $full; 2511 $ENV{"REQUEST_URI"} = $request_uri; 2512 $ENV{"PATH_INFO"} = $pathinfo; 2513 if ($pathinfo) { 2514 $ENV{"PATH_TRANSLATED"} = "$roots[0]$pathinfo"; 2515 $ENV{"PATH_REALTRANSLATED"} = "$realroot$pathinfo"; 2516 } 2517 $ENV{"QUERY_STRING"} = $querystring; 2518 $ENV{"MINISERV_CONFIG"} = $config_file; 2519 $ENV{"HTTPS"} = $use_ssl || $config{'inetd_ssl'} ? "ON" : ""; 2520 $ENV{"MINISERV_PID"} = $miniserv_main_pid; 2521 $ENV{"SESSION_ID"} = $session_id if ($session_id); 2522 $ENV{"LOCAL_USER"} = $localauth_user if ($localauth_user); 2523 $ENV{"MINISERV_INTERNAL"} = $miniserv_internal if ($miniserv_internal); 2524 if (defined($header{"content-length"})) { 2525 $ENV{"CONTENT_LENGTH"} = $header{"content-length"}; 2526 } 2527 if (defined($header{"content-type"})) { 2528 $ENV{"CONTENT_TYPE"} = $header{"content-type"}; 2529 } 2530 foreach $h (keys %header) { 2531 ($hname = $h) =~ tr/a-z/A-Z/; 2532 $hname =~ s/\-/_/g; 2533 $ENV{"HTTP_$hname"} = $header{$h}; 2534 } 2535 $ENV{"PWD"} = $cgi_pwd; 2536 foreach $k (keys %config) { 2537 if ($k =~ /^env_(\S+)$/) { 2538 $ENV{$1} = $config{$k}; 2539 } 2540 } 2541 delete($ENV{'HTTP_AUTHORIZATION'}); 2542 $ENV{'HTTP_COOKIE'} =~ s/;?\s*$sidname=([a-f0-9]+)//; 2543 $ENV{'MOBILE_DEVICE'} = 1 if ($mobile_device); 2544 2545 # Check if the CGI can be handled internally 2546 open(CGI, $full); 2547 local $first = <CGI>; 2548 close(CGI); 2549 $first =~ s/[#!\r\n]//g; 2550 $nph_script = ($full =~ /\/nph-([^\/]+)$/); 2551 seek(STDERR, 0, 2); 2552 if (!$config{'forkcgis'} && 2553 ($first eq $perl_path || $first eq $linked_perl_path || 2554 $first =~ /\/perl$/ || $first =~ /^\/\S+\/env\s+perl$/) && 2555 $] >= 5.004 || 2556 $config{'internalcgis'}) { 2557 # setup environment for eval 2558 chdir($ENV{"PWD"}); 2559 @ARGV = split(/\s+/, $queryargs); 2560 $0 = $full; 2561 if ($posted_data) { 2562 # Already read the post input 2563 $postinput = $posted_data; 2564 } 2565 $clen = $header{"content-length"}; 2566 $SIG{'CHLD'} = 'DEFAULT'; 2567 eval { 2568 # Have SOCK closed if the perl exec's something 2569 use Fcntl; 2570 fcntl(SOCK, F_SETFD, FD_CLOEXEC); 2571 }; 2572 #shutdown(SOCK, 0); 2573 2574 if ($config{'log'}) { 2575 open(MINISERVLOG, ">>$config{'logfile'}"); 2576 if ($config{'logperms'}) { 2577 chmod(oct($config{'logperms'}), 2578 $config{'logfile'}); 2579 } 2580 else { 2581 chmod(0600, $config{'logfile'}); 2582 } 2583 } 2584 $doing_cgi_eval = 1; 2585 $main_process_id = $$; 2586 $pkg = "main"; 2587 if ($full =~ /^\Q$foundroot\E\/([^\/]+)\//) { 2588 # Eval in package from Webmin module name 2589 $pkg = $1; 2590 $pkg =~ s/[^A-Za-z0-9]/_/g; 2591 } 2592 eval " 2593 \%pkg::ENV = \%ENV; 2594 package $pkg; 2595 tie(*STDOUT, 'miniserv'); 2596 tie(*STDIN, 'miniserv'); 2597 do \$miniserv::full; 2598 die \$@ if (\$@); 2599 "; 2600 $doing_cgi_eval = 0; 2601 if ($@) { 2602 # Error in perl! 2603 &http_error(500, "Perl execution failed", 2604 $config{'noshowstderr'} ? undef : "$@"); 2605 } 2606 elsif (!$doneheaders && !$nph_script) { 2607 &http_error(500, "Missing Headers"); 2608 } 2609 $rv = 0; 2610 } 2611 else { 2612 $infile = undef; 2613 if (!$on_windows) { 2614 # fork the process that actually executes the CGI 2615 pipe(CGIINr, CGIINw); 2616 pipe(CGIOUTr, CGIOUTw); 2617 pipe(CGIERRr, CGIERRw); 2618 if (!($cgipid = fork())) { 2619 @execargs = ( $full, split(/\s+/, $queryargs) ); 2620 chdir($ENV{"PWD"}); 2621 close(SOCK); 2622 open(STDIN, "<&CGIINr"); 2623 open(STDOUT, ">&CGIOUTw"); 2624 open(STDERR, ">&CGIERRw"); 2625 close(CGIINw); close(CGIOUTr); close(CGIERRr); 2626 exec(@execargs) || 2627 die "Failed to exec $full : $!\n"; 2628 exit(0); 2629 } 2630 close(CGIINr); close(CGIOUTw); close(CGIERRw); 2631 } 2632 else { 2633 # write CGI input to a temp file 2634 $infile = "$config{'tempbase'}.$$"; 2635 open(CGIINw, ">$infile"); 2636 # NOT binary mode, as CGIs don't read in it! 2637 } 2638 2639 # send post data 2640 if ($posted_data) { 2641 # already read the posted data 2642 print CGIINw $posted_data; 2643 } 2644 $clen = $header{"content-length"}; 2645 if ($method eq "POST" && $clen_read < $clen) { 2646 $SIG{'PIPE'} = 'IGNORE'; 2647 $got = $clen_read; 2648 while($got < $clen) { 2649 $buf = &read_data($clen-$got); 2650 if (!length($buf)) { 2651 kill('TERM', $cgipid); 2652 unlink($infile) if ($infile); 2653 &http_error(500, "Failed to read ". 2654 "POST request"); 2655 } 2656 $got += length($buf); 2657 local ($wrote) = (print CGIINw $buf); 2658 last if (!$wrote); 2659 } 2660 # If the CGI terminated early, we still need to read 2661 # from the browser and throw away 2662 while($got < $clen) { 2663 $buf = &read_data($clen-$got); 2664 if (!length($buf)) { 2665 kill('TERM', $cgipid); 2666 unlink($infile) if ($infile); 2667 &http_error(500, "Failed to read ". 2668 "POST request"); 2669 } 2670 $got += length($buf); 2671 } 2672 $SIG{'PIPE'} = 'DEFAULT'; 2673 } 2674 close(CGIINw); 2675 shutdown(SOCK, 0); 2676 2677 if ($on_windows) { 2678 # Run the CGI program, and feed it input 2679 chdir($ENV{"PWD"}); 2680 local $qqueryargs = join(" ", 2681 map { s/([<>|&"^])/^$1/g; "\"$_\"" } 2682 split(/\s+/, $queryargs)); 2683 if ($first =~ /(perl|perl.exe)$/i) { 2684 # On Windows, run with Perl 2685 open(CGIOUTr, "$perl_path \"$full\" $qqueryargs <$infile |"); 2686 } 2687 else { 2688 open(CGIOUTr, "\"$full\" $qqueryargs <$infile |"); 2689 } 2690 binmode(CGIOUTr); 2691 } 2692 2693 if (!$nph_script) { 2694 # read back cgi headers 2695 select(CGIOUTr); $|=1; select(STDOUT); 2696 $got_blank = 0; 2697 while(1) { 2698 $line = <CGIOUTr>; 2699 $line =~ s/\r|\n//g; 2700 if ($line eq "") { 2701 if ($got_blank || %cgiheader) { last; } 2702 $got_blank++; 2703 next; 2704 } 2705 if ($line !~ /^(\S+):\s+(.*)$/) { 2706 $errs = &read_errors(CGIERRr); 2707 close(CGIOUTr); close(CGIERRr); 2708 unlink($infile) if ($infile); 2709 &http_error(500, "Bad Header", $errs); 2710 } 2711 $cgiheader{lc($1)} = $2; 2712 push(@cgiheader, [ $1, $2 ]); 2713 } 2714 if ($cgiheader{"location"}) { 2715 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 2716 &write_data("Date: $datestr\r\n"); 2717 &write_data("Server: $config{'server'}\r\n"); 2718 &write_keep_alive(0); 2719 # ignore the rest of the output. This is a hack, 2720 # but is necessary for IE in some cases :( 2721 close(CGIOUTr); close(CGIERRr); 2722 } 2723 elsif ($cgiheader{"content-type"} eq "") { 2724 close(CGIOUTr); close(CGIERRr); 2725 unlink($infile) if ($infile); 2726 $errs = &read_errors(CGIERRr); 2727 &http_error(500, "Missing Content-Type Header", 2728 $config{'noshowstderr'} ? undef : $errs); 2729 } 2730 else { 2731 &write_data("HTTP/1.0 $ok_code $ok_message\r\n"); 2732 &write_data("Date: $datestr\r\n"); 2733 &write_data("Server: $config{'server'}\r\n"); 2734 &write_keep_alive(0); 2735 } 2736 foreach $h (@cgiheader) { 2737 &write_data("$h->[0]: $h->[1]\r\n"); 2738 } 2739 &write_data("\r\n"); 2740 } 2741 &reset_byte_count(); 2742 while($line = <CGIOUTr>) { 2743 &write_data($line); 2744 } 2745 close(CGIOUTr); 2746 close(CGIERRr); 2747 unlink($infile) if ($infile); 2748 $rv = 0; 2749 } 2750 } 2751else { 2752 # A file to output 2753 print DEBUG "handle_request: outputting file $full\n"; 2754 $gzfile = $full.".gz"; 2755 $gzipped = 0; 2756 if ($config{'gzip'} ne '0' && -r $gzfile && $acceptenc{'gzip'}) { 2757 # Using gzipped version 2758 @stopen = stat($gzfile); 2759 if ($stopen[9] >= $stfull[9] && open(FILE, $gzfile)) { 2760 print DEBUG "handle_request: using gzipped $gzfile\n"; 2761 $gzipped = 1; 2762 } 2763 } 2764 if (!$gzipped) { 2765 # Using original file 2766 @stopen = @stfull; 2767 open(FILE, $full) || &http_error(404, "Failed to open file"); 2768 } 2769 binmode(FILE); 2770 2771 # Build common headers 2772 local $etime = &get_expires_time($simple); 2773 local $resp = "HTTP/1.0 $ok_code $ok_message\r\n". 2774 "Date: $datestr\r\n". 2775 "Server: $config{server}\r\n". 2776 "Content-type: ".&get_type($full)."\r\n". 2777 "Last-Modified: ".&http_date($stopen[9])."\r\n". 2778 "Expires: ".&http_date(time()+$etime)."\r\n". 2779 "Cache-Control: public; max-age=".$etime."\r\n"; 2780 2781 if (!$gzipped && $use_gzip && $acceptenc{'gzip'} && 2782 &should_gzip_file($full)) { 2783 # Load and compress file, then output 2784 print DEBUG "handle_request: outputting gzipped file $full\n"; 2785 open(FILE, $full) || &http_error(404, "Failed to open file"); 2786 { 2787 local $/ = undef; 2788 $data = <FILE>; 2789 } 2790 close(FILE); 2791 @stopen = stat($file); 2792 $data = Compress::Zlib::memGzip($data); 2793 $resp .= "Content-length: ".length($data)."\r\n". 2794 "Content-Encoding: gzip\r\n"; 2795 &write_data($resp); 2796 $rv = &write_keep_alive(); 2797 &write_data("\r\n"); 2798 &reset_byte_count(); 2799 &write_data($data); 2800 } 2801 else { 2802 # Stream file output 2803 $resp .= "Content-length: $stopen[7]\r\n"; 2804 $resp .= "Content-Encoding: gzip\r\n" if ($gzipped); 2805 &write_data($resp); 2806 $rv = &write_keep_alive(); 2807 &write_data("\r\n"); 2808 &reset_byte_count(); 2809 my $bufsize = $config{'bufsize'} || 32768; 2810 while(read(FILE, $buf, $bufsize) > 0) { 2811 &write_data($buf); 2812 } 2813 close(FILE); 2814 } 2815 } 2816 2817# log the request 2818&log_request($loghost, $authuser, $reqline, 2819 $logged_code ? $logged_code : 2820 $cgiheader{"location"} ? "302" : $ok_code, &byte_count()); 2821return $rv; 2822} 2823 2824# http_error(code, message, body, [dontexit], [dontstderr]) 2825# Output an error message to the browser, and log it to the error log 2826sub http_error 2827{ 2828my ($code, $msg, $body, $noexit, $noerr) = @_; 2829local $eh = $error_handler_recurse ? undef : 2830 $config{"error_handler_".$code} ? $config{"error_handler_".$code} : 2831 $config{'error_handler'} ? $config{'error_handler'} : undef; 2832print DEBUG "http_error code=$code message=$msg body=$body\n"; 2833if ($eh) { 2834 # Call a CGI program for the error 2835 $page = "/$eh"; 2836 $querystring = "code=$_[0]&message=".&urlize($msg). 2837 "&body=".&urlize($body); 2838 $error_handler_recurse++; 2839 $ok_code = $code; 2840 $ok_message = $msg; 2841 goto rerun; 2842 } 2843else { 2844 # Use the standard error message display 2845 &write_data("HTTP/1.0 $code $msg\r\n"); 2846 &write_data("Server: $config{server}\r\n"); 2847 &write_data("Date: $datestr\r\n"); 2848 &write_data("Content-type: text/html; Charset=utf-8\r\n"); 2849 &write_keep_alive(0); 2850 &write_data("\r\n"); 2851 &reset_byte_count(); 2852 &write_data("<html>\n"); 2853 &write_data("<head>".&embed_error_styles($roots[0])."<title>$code — $msg</title></head>\n"); 2854 &write_data("<body class=\"err-body\"><h2 class=\"err-head\">Error — $msg</h2>\n"); 2855 if ($body) { 2856 &write_data("<p class=\"err-content\">$body</p>\n"); 2857 } 2858 &write_data("</body></html>\n"); 2859 } 2860&log_request($loghost, $authuser, $reqline, $code, &byte_count()) 2861 if ($reqline); 2862&log_error($msg, $body ? " : $body" : "") if (!$noerr); 2863shutdown(SOCK, 1); 2864exit if (!$noexit); 2865} 2866 2867# embed_error_styles() 2868# Returns HTML styles for nicer errors. For internal use only. 2869sub embed_error_styles 2870{ 2871my ($root) = @_; 2872if ($root) { 2873 my $err_style = &read_any_file("$root/unauthenticated/errors.css"); 2874 if ($err_style) { 2875 $err_style =~ s/[\n\r]//g; 2876 $err_style =~ s/\s+/ /g; 2877 $err_style = "<style data-err type=\"text/css\">$err_style</style>"; 2878 return "\n$err_style\n"; 2879 } 2880 } 2881return undef; 2882} 2883 2884sub get_type 2885{ 2886if ($_[0] =~ /\.([A-z0-9]+)$/) { 2887 $t = $mime{$1}; 2888 if ($t ne "") { 2889 return $t; 2890 } 2891 } 2892return "text/plain"; 2893} 2894 2895# simplify_path(path, bogus) 2896# Given a path, maybe containing stuff like ".." and "." convert it to a 2897# clean, absolute form. 2898sub simplify_path 2899{ 2900local($dir, @bits, @fixedbits, $b); 2901$dir = $_[0]; 2902$dir =~ s/\\/\//g; # fix windows \ in path 2903$dir =~ s/^\/+//g; 2904$dir =~ s/\/+$//g; 2905$dir =~ s/\0//g; # remove null bytes 2906@bits = split(/\/+/, $dir); 2907@fixedbits = (); 2908$_[1] = 0; 2909foreach $b (@bits) { 2910 if ($b eq ".") { 2911 # Do nothing.. 2912 } 2913 elsif ($b eq ".." || $b eq "...") { 2914 # Remove last dir 2915 if (scalar(@fixedbits) == 0) { 2916 $_[1] = 1; 2917 return "/"; 2918 } 2919 pop(@fixedbits); 2920 } 2921 else { 2922 # Add dir to list 2923 push(@fixedbits, $b); 2924 } 2925 } 2926return "/" . join('/', @fixedbits); 2927} 2928 2929# b64decode(string) 2930# Converts a string from base64 format to normal 2931sub b64decode 2932{ 2933 local($str) = $_[0]; 2934 local($res); 2935 $str =~ tr|A-Za-z0-9+=/||cd; 2936 $str =~ s/=+$//; 2937 $str =~ tr|A-Za-z0-9+/| -_|; 2938 while ($str =~ /(.{1,60})/gs) { 2939 my $len = chr(32 + length($1)*3/4); 2940 $res .= unpack("u", $len . $1 ); 2941 } 2942 return $res; 2943} 2944 2945# ip_match(remoteip, localip, [match]+) 2946# Checks an IP address against a list of IPs, networks and networks/masks 2947sub ip_match 2948{ 2949local(@io, @mo, @ms, $i, $j, $hn, $needhn); 2950@io = &check_ip6address($_[0]) ? split(/:/, $_[0]) 2951 : split(/\./, $_[0]); 2952for($i=2; $i<@_; $i++) { 2953 $needhn++ if ($_[$i] =~ /^\*(\S+)$/); 2954 } 2955if ($needhn && !defined($hn = $ip_match_cache{$_[0]})) { 2956 # Reverse-lookup hostname if any rules match based on it 2957 $hn = &to_hostname($_[0]); 2958 if (&check_ip6address($_[0])) { 2959 $hn = "" if (&to_ip6address($hn) ne $_[0]); 2960 } 2961 else { 2962 $hn = "" if (&to_ipaddress($hn) ne $_[0]); 2963 } 2964 $ip_match_cache{$_[0]} = $hn; 2965 } 2966for($i=2; $i<@_; $i++) { 2967 local $mismatch = 0; 2968 if ($_[$i] =~ /^([0-9\.]+)\/(\d+)$/) { 2969 # Convert CIDR to netmask format 2970 $_[$i] = $1."/".&prefix_to_mask($2); 2971 } 2972 if ($_[$i] =~ /^([0-9\.]+)\/([0-9\.]+)$/) { 2973 # Compare with IPv4 network/mask 2974 @mo = split(/\./, $1); 2975 @ms = split(/\./, $2); 2976 for($j=0; $j<4; $j++) { 2977 if ((int($io[$j]) & int($ms[$j])) != (int($mo[$j]) & int($ms[$j]))) { 2978 $mismatch = 1; 2979 } 2980 } 2981 } 2982 elsif ($_[$i] =~ /^([0-9\.]+)-([0-9\.]+)$/) { 2983 # Compare with an IPv4 range (separated by a hyphen -) 2984 local ($remote, $min, $max); 2985 local @low = split(/\./, $1); 2986 local @high = split(/\./, $2); 2987 for($j=0; $j<4; $j++) { 2988 $remote += $io[$j] << ((3-$j)*8); 2989 $min += $low[$j] << ((3-$j)*8); 2990 $max += $high[$j] << ((3-$j)*8); 2991 } 2992 if ($remote < $min || $remote > $max) { 2993 $mismatch = 1; 2994 } 2995 } 2996 elsif ($_[$i] =~ /^\*(\S+)$/) { 2997 # Compare with hostname regexp 2998 $mismatch = 1 if ($hn !~ /^.*\Q$1\E$/i); 2999 } 3000 elsif ($_[$i] eq 'LOCAL' && &check_ipaddress($_[1])) { 3001 # Compare with local IPv4 network 3002 local @lo = split(/\./, $_[1]); 3003 if ($lo[0] < 128) { 3004 $mismatch = 1 if ($lo[0] != $io[0]); 3005 } 3006 elsif ($lo[0] < 192) { 3007 $mismatch = 1 if ($lo[0] != $io[0] || 3008 $lo[1] != $io[1]); 3009 } 3010 else { 3011 $mismatch = 1 if ($lo[0] != $io[0] || 3012 $lo[1] != $io[1] || 3013 $lo[2] != $io[2]); 3014 } 3015 } 3016 elsif ($_[$i] eq 'LOCAL' && &check_ip6address($_[1])) { 3017 # Compare with local IPv6 network, which is always first 4 words 3018 local @lo = split(/:/, $_[1]); 3019 for(my $i=0; $i<4; $i++) { 3020 $mismatch = 1 if ($lo[$i] ne $io[$i]); 3021 } 3022 } 3023 elsif ($_[$i] =~ /^[0-9\.]+$/) { 3024 # Compare with a full or partial IPv4 address 3025 @mo = split(/\./, $_[$i]); 3026 while(@mo && !$mo[$#mo]) { pop(@mo); } 3027 for($j=0; $j<@mo; $j++) { 3028 if ($mo[$j] != $io[$j]) { 3029 $mismatch = 1; 3030 } 3031 } 3032 } 3033 elsif ($_[$i] =~ /^[a-f0-9:]+$/) { 3034 # Compare with a full IPv6 address 3035 if (&canonicalize_ip6($_[$i]) ne canonicalize_ip6($_[0])) { 3036 $mismatch = 1; 3037 } 3038 } 3039 elsif ($_[$i] =~ /^([a-f0-9:]+)\/(\d+)$/) { 3040 # Compare with an IPv6 network 3041 local $v6size = $2; 3042 local $v6addr = &canonicalize_ip6($1); 3043 local $bytes = $v6size / 8; 3044 @mo = &expand_ipv6_bytes($v6addr); 3045 local @io6 = &expand_ipv6_bytes(&canonicalize_ip6($_[0])); 3046 for($j=0; $j<$bytes; $j++) { 3047 if ($mo[$j] ne $io6[$j]) { 3048 $mismatch = 1; 3049 } 3050 } 3051 } 3052 elsif ($_[$i] !~ /^[0-9\.]+$/) { 3053 # Compare with hostname 3054 $mismatch = 1 if ($_[0] ne &to_ipaddress($_[$i])); 3055 } 3056 return 1 if (!$mismatch); 3057 } 3058return 0; 3059} 3060 3061# users_match(&uinfo, user, ...) 3062# Returns 1 if a user is in a list of users and groups 3063sub users_match 3064{ 3065local $uinfo = shift(@_); 3066local $u; 3067local @ginfo = getgrgid($uinfo->[3]); 3068foreach $u (@_) { 3069 if ($u =~ /^\@(\S+)$/) { 3070 return 1 if (&is_group_member($uinfo, $1)); 3071 } 3072 elsif ($u =~ /^(\d*)-(\d*)$/ && ($1 || $2)) { 3073 return (!$1 || $uinfo[2] >= $1) && 3074 (!$2 || $uinfo[2] <= $2); 3075 } 3076 else { 3077 return 1 if ($u eq $uinfo->[0]); 3078 } 3079 } 3080return 0; 3081} 3082 3083# restart_miniserv() 3084# Called when a SIGHUP is received to restart the web server. This is done 3085# by exec()ing perl with the same command line as was originally used 3086sub restart_miniserv 3087{ 3088&log_error("Restarting"); 3089close(SOCK); 3090&close_all_sockets(); 3091&close_all_pipes(); 3092dbmclose(%sessiondb); 3093kill('KILL', $logclearer) if ($logclearer); 3094kill('KILL', $extauth) if ($extauth); 3095exec($perl_path, $miniserv_path, @miniserv_argv); 3096die "Failed to restart miniserv with $perl_path $miniserv_path"; 3097} 3098 3099sub trigger_restart 3100{ 3101$need_restart = 1; 3102} 3103 3104sub trigger_reload 3105{ 3106$need_reload = 1; 3107} 3108 3109# to_ip46address(address, ...) 3110# Convert hostnames to v4 and v6 addresses, if possible 3111sub to_ip46address 3112{ 3113local @rv; 3114foreach my $i (@_) { 3115 if (&check_ipaddress($i) || &check_ip6address($i)) { 3116 push(@rv, $i); 3117 } 3118 else { 3119 my $addr = &to_ipaddress($i); 3120 $addr ||= &to_ip6address($i); 3121 push(@rv, $addr) if ($addr); 3122 } 3123 } 3124return @rv; 3125} 3126 3127# to_ipaddress(address, ...) 3128sub to_ipaddress 3129{ 3130local (@rv, $i); 3131foreach $i (@_) { 3132 if ($i =~ /(\S+)\/(\S+)/ || $i =~ /^\*\S+$/ || 3133 $i eq 'LOCAL' || $i =~ /^[0-9\.]+$/ || $i =~ /^[a-f0-9:]+$/) { 3134 # A pattern or IP, not a hostname, so don't change 3135 push(@rv, $i); 3136 } 3137 else { 3138 # Lookup IP address 3139 push(@rv, join('.', unpack("CCCC", inet_aton($i)))); 3140 } 3141 } 3142return wantarray ? @rv : $rv[0]; 3143} 3144 3145# to_ip6address(address, ...) 3146sub to_ip6address 3147{ 3148local (@rv, $i); 3149foreach $i (@_) { 3150 if ($i =~ /(\S+)\/(\S+)/ || $i =~ /^\*\S+$/ || 3151 $i eq 'LOCAL' || $i =~ /^[0-9\.]+$/ || $i =~ /^[a-f0-9:]+$/) { 3152 # A pattern, not a hostname, so don't change 3153 push(@rv, $i); 3154 } 3155 elsif ($config{'ipv6'}) { 3156 # Lookup IPv6 address 3157 local ($inaddr, $addr); 3158 eval { 3159 (undef, undef, undef, $inaddr) = 3160 getaddrinfo($i, undef, AF_INET6(), SOCK_STREAM); 3161 }; 3162 if ($inaddr) { 3163 push(@rv, undef); 3164 } 3165 else { 3166 (undef, $addr) = unpack_sockaddr_in6($inaddr); 3167 push(@rv, inet_ntop(AF_INET6(), $addr)); 3168 } 3169 } 3170 } 3171return wantarray ? @rv : $rv[0]; 3172} 3173 3174# to_hostname(ipv4|ipv6-address) 3175# Reverse-resolves an IPv4 or 6 address to a hostname 3176sub to_hostname 3177{ 3178local ($addr) = @_; 3179if (&check_ip6address($_[0])) { 3180 return gethostbyaddr(inet_pton(AF_INET6(), $addr), 3181 AF_INET6()); 3182 } 3183else { 3184 return gethostbyaddr(inet_aton($addr), AF_INET); 3185 } 3186} 3187 3188# read_line(no-wait, no-limit) 3189# Reads one line from SOCK or SSL 3190sub read_line 3191{ 3192local ($nowait, $nolimit) = @_; 3193local($idx, $more, $rv); 3194while(($idx = index($main::read_buffer, "\n")) < 0) { 3195 if (length($main::read_buffer) > 100000 && !$nolimit) { 3196 &http_error(414, "Request too long", 3197 "Received excessive line <pre class=\"err-content\">".&html_strip($main::read_buffer)."</pre>"); 3198 } 3199 3200 # need to read more.. 3201 &wait_for_data_error() if (!$nowait); 3202 if ($use_ssl) { 3203 $more = Net::SSLeay::read($ssl_con); 3204 } 3205 else { 3206 my $bufsize = $config{'bufsize'} || 32768; 3207 local $ok = sysread(SOCK, $more, $bufsize); 3208 $more = undef if ($ok <= 0); 3209 } 3210 if ($more eq '') { 3211 # end of the data 3212 $rv = $main::read_buffer; 3213 undef($main::read_buffer); 3214 return $rv; 3215 } 3216 $main::read_buffer .= $more; 3217 } 3218$rv = substr($main::read_buffer, 0, $idx+1); 3219$main::read_buffer = substr($main::read_buffer, $idx+1); 3220return $rv; 3221} 3222 3223# read_data(length) 3224# Reads up to some amount of data from SOCK or the SSL connection 3225sub read_data 3226{ 3227local ($rv); 3228if (length($main::read_buffer)) { 3229 if (length($main::read_buffer) > $_[0]) { 3230 # Return the first part of the buffer 3231 $rv = substr($main::read_buffer, 0, $_[0]); 3232 $main::read_buffer = substr($main::read_buffer, $_[0]); 3233 return $rv; 3234 } 3235 else { 3236 # Return the whole buffer 3237 $rv = $main::read_buffer; 3238 undef($main::read_buffer); 3239 return $rv; 3240 } 3241 } 3242elsif ($use_ssl) { 3243 # Call SSL read function 3244 return Net::SSLeay::read($ssl_con, $_[0]); 3245 } 3246else { 3247 # Just do a normal read 3248 local $buf; 3249 sysread(SOCK, $buf, $_[0]) || return undef; 3250 return $buf; 3251 } 3252} 3253 3254# wait_for_data(secs) 3255# Waits at most the given amount of time for some data on SOCK, returning 3256# 0 if not found, 1 if some arrived. 3257sub wait_for_data 3258{ 3259local $rmask; 3260vec($rmask, fileno(SOCK), 1) = 1; 3261local $got = select($rmask, undef, undef, $_[0]); 3262return $got == 0 ? 0 : 1; 3263} 3264 3265# wait_for_data_error() 3266# Waits 60 seconds for data on SOCK, and fails if none arrives 3267sub wait_for_data_error 3268{ 3269local $got = &wait_for_data(60); 3270if (!$got) { 3271 &http_error(400, "Timeout", 3272 "Waited more than 60 seconds for request data"); 3273 } 3274} 3275 3276# write_data(data, ...) 3277# Writes a string to SOCK or the SSL connection 3278sub write_data 3279{ 3280local $str = join("", @_); 3281if ($use_ssl) { 3282 Net::SSLeay::write($ssl_con, $str); 3283 } 3284else { 3285 eval { syswrite(SOCK, $str, length($str)); }; 3286 if ($@ =~ /wide\s+character/i) { 3287 eval { utf8::encode($str); 3288 syswrite(SOCK, $str, length($str)); }; 3289 } 3290 if ($@) { 3291 # Somehow a string come through that contains invalid chars 3292 print STDERR $@,"\n"; 3293 for(my $i=0; my @stack = caller($i); $i++) { 3294 print STDERR join(" ", @stack),"\n"; 3295 } 3296 } 3297 } 3298$write_data_count += length($str); 3299} 3300 3301# reset_byte_count() 3302sub reset_byte_count { $write_data_count = 0; } 3303 3304# byte_count() 3305sub byte_count { return $write_data_count; } 3306 3307# log_request(hostname, user, request, code, bytes) 3308# Write an HTTP request to the log file 3309sub log_request 3310{ 3311local ($host, $user, $request, $code, $bytes) = @_; 3312local $headers; 3313my $request_nolog = $request; 3314 3315# Process full request string like `POST /index.cgi?param=1 HTTP/1.1` as well 3316if ($request =~ /^(POST|GET)\s+/) { 3317 $request_nolog =~ s/(.*?)(\/.*?)\s+(.*)/$2/g; 3318 } 3319if ($config{'nolog'}) { 3320 foreach my $nolog (split(/\s+/, $config{'nolog'})) { 3321 return if ($request_nolog =~ /^$nolog$/); 3322 } 3323 } 3324if ($config{'log'}) { 3325 local $ident = "-"; 3326 $user ||= "-"; 3327 local $dstr = &make_datestr(); 3328 if (fileno(MINISERVLOG)) { 3329 seek(MINISERVLOG, 0, 2); 3330 } 3331 else { 3332 open(MINISERVLOG, ">>$config{'logfile'}"); 3333 chmod(0600, $config{'logfile'}); 3334 } 3335 if (defined($config{'logheaders'})) { 3336 foreach $h (split(/\s+/, $config{'logheaders'})) { 3337 $headers .= " $h=\"$header{$h}\""; 3338 } 3339 } 3340 elsif ($config{'logclf'}) { 3341 $headers = " \"$header{'referer'}\" \"$header{'user-agent'}\""; 3342 } 3343 else { 3344 $headers = ""; 3345 } 3346 print MINISERVLOG "$host $ident $user [$dstr] \"$request\" ", 3347 "$code $bytes$headers\n"; 3348 close(MINISERVLOG); 3349 } 3350} 3351 3352# make_datestr() 3353sub make_datestr 3354{ 3355local @tm = localtime(time()); 3356return sprintf "%2.2d/%s/%4.4d:%2.2d:%2.2d:%2.2d %s", 3357 $tm[3], $month[$tm[4]], $tm[5]+1900, 3358 $tm[2], $tm[1], $tm[0], $timezone; 3359} 3360 3361# log_error(message) 3362sub log_error 3363{ 3364seek(STDERR, 0, 2); 3365print STDERR "[",&make_datestr(),"] ", 3366 $acpthost ? ( "[",$acpthost,"] " ) : ( ), 3367 $page ? ( $page," : " ) : ( ), 3368 @_,"\n"; 3369} 3370 3371# read_errors(handle) 3372# Read and return all input from some filehandle 3373sub read_errors 3374{ 3375local($fh, $_, $rv); 3376$fh = $_[0]; 3377while(<$fh>) { $rv .= $_; } 3378return $rv; 3379} 3380 3381sub write_keep_alive 3382{ 3383local $mode; 3384if ($config{'nokeepalive'}) { 3385 # Keep alives have been disabled in config 3386 $mode = 0; 3387 } 3388elsif (@childpids > $config{'maxconns'}*.8) { 3389 # Disable because nearing process limit 3390 $mode = 0; 3391 } 3392elsif (@_) { 3393 # Keep alive specified by caller 3394 $mode = $_[0]; 3395 } 3396else { 3397 # Keep alive determined by browser 3398 $mode = $header{'connection'} =~ /keep-alive/i; 3399 } 3400&write_data("Connection: ".($mode ? "Keep-Alive" : "close")."\r\n"); 3401return $mode; 3402} 3403 3404sub term_handler 3405{ 3406kill('TERM', @childpids) if (@childpids); 3407kill('KILL', $logclearer) if ($logclearer); 3408kill('KILL', $extauth) if ($extauth); 3409exit(1); 3410} 3411 3412sub http_date 3413{ 3414local @tm = gmtime($_[0]); 3415return sprintf "%s, %d %s %d %2.2d:%2.2d:%2.2d GMT", 3416 $weekday[$tm[6]], $tm[3], $month[$tm[4]], $tm[5]+1900, 3417 $tm[2], $tm[1], $tm[0]; 3418} 3419 3420sub TIEHANDLE 3421{ 3422my $i; bless \$i, shift; 3423} 3424 3425sub WRITE 3426{ 3427$r = shift; 3428my($buf,$len,$offset) = @_; 3429&write_to_sock(substr($buf, $offset, $len)); 3430$miniserv::page_capture_out .= substr($buf, $offset, $len) 3431 if ($miniserv::page_capture); 3432} 3433 3434sub PRINT 3435{ 3436$r = shift; 3437$$r++; 3438my $buf = join(defined($,) ? $, : "", @_); 3439$buf .= $\ if defined($\); 3440&write_to_sock($buf); 3441$miniserv::page_capture_out .= $buf 3442 if ($miniserv::page_capture); 3443} 3444 3445sub PRINTF 3446{ 3447shift; 3448my $fmt = shift; 3449my $buf = sprintf $fmt, @_; 3450&write_to_sock($buf); 3451$miniserv::page_capture_out .= $buf 3452 if ($miniserv::page_capture); 3453} 3454 3455# Send back already read data while we have it, then read from SOCK 3456sub READ 3457{ 3458my $r = shift; 3459my $bufref = \$_[0]; 3460my $len = $_[1]; 3461my $offset = $_[2]; 3462if ($postpos < length($postinput)) { 3463 # Reading from already fetched array 3464 my $left = length($postinput) - $postpos; 3465 my $canread = $len > $left ? $left : $len; 3466 substr($$bufref, $offset, $canread) = 3467 substr($postinput, $postpos, $canread); 3468 $postpos += $canread; 3469 return $canread; 3470 } 3471else { 3472 # Read from network socket 3473 local $data = &read_data($len); 3474 if ($data eq '' && $len) { 3475 # End of socket 3476 shutdown(SOCK, 0); 3477 } 3478 substr($$bufref, $offset, length($data)) = $data; 3479 return length($data); 3480 } 3481} 3482 3483sub OPEN 3484{ 3485#print STDERR "open() called - should never happen!\n"; 3486} 3487 3488# Read a line of input 3489sub READLINE 3490{ 3491my $r = shift; 3492if ($postpos < length($postinput) && 3493 ($idx = index($postinput, "\n", $postpos)) >= 0) { 3494 # A line exists in the memory buffer .. use it 3495 my $line = substr($postinput, $postpos, $idx-$postpos+1); 3496 $postpos = $idx+1; 3497 return $line; 3498 } 3499else { 3500 # Need to read from the socket 3501 my $line; 3502 if ($postpos < length($postinput)) { 3503 # Start with in-memory data 3504 $line = substr($postinput, $postpos); 3505 $postpos = length($postinput); 3506 } 3507 my $nl = &read_line(0, 1); 3508 if ($nl eq '') { 3509 # End of socket 3510 shutdown(SOCK, 0); 3511 } 3512 $line .= $nl if (defined($nl)); 3513 return $line; 3514 } 3515} 3516 3517# Read one character of input 3518sub GETC 3519{ 3520my $r = shift; 3521my $buf; 3522my $got = READ($r, \$buf, 1, 0); 3523return $got > 0 ? $buf : undef; 3524} 3525 3526sub FILENO 3527{ 3528return fileno(SOCK); 3529} 3530 3531sub CLOSE { } 3532 3533sub DESTROY { } 3534 3535# write_to_sock(data, ...) 3536sub write_to_sock 3537{ 3538local $d; 3539foreach $d (@_) { 3540 if ($doneheaders || $miniserv::nph_script) { 3541 &write_data($d); 3542 } 3543 else { 3544 $headers .= $d; 3545 while(!$doneheaders && $headers =~ s/^([^\r\n]*)(\r)?\n//) { 3546 if ($1 =~ /^(\S+):\s+(.*)$/) { 3547 $cgiheader{lc($1)} = $2; 3548 push(@cgiheader, [ $1, $2 ]); 3549 } 3550 elsif ($1 !~ /\S/) { 3551 $doneheaders++; 3552 } 3553 else { 3554 &http_error(500, "Bad Header"); 3555 } 3556 } 3557 if ($doneheaders) { 3558 if ($cgiheader{"location"}) { 3559 &write_data( 3560 "HTTP/1.0 302 Moved Temporarily\r\n"); 3561 &write_data("Date: $datestr\r\n"); 3562 &write_data("Server: $config{server}\r\n"); 3563 &write_keep_alive(0); 3564 } 3565 elsif ($cgiheader{"content-type"} eq "") { 3566 &http_error(500, "Missing Content-Type Header"); 3567 } 3568 else { 3569 &write_data("HTTP/1.0 $ok_code $ok_message\r\n"); 3570 &write_data("Date: $datestr\r\n"); 3571 &write_data("Server: $config{server}\r\n"); 3572 &write_keep_alive(0); 3573 } 3574 foreach $h (@cgiheader) { 3575 &write_data("$h->[0]: $h->[1]\r\n"); 3576 } 3577 &write_data("\r\n"); 3578 &reset_byte_count(); 3579 &write_data($headers); 3580 } 3581 } 3582 } 3583} 3584 3585sub verify_client 3586{ 3587local $cert = Net::SSLeay::X509_STORE_CTX_get_current_cert($_[1]); 3588if ($cert) { 3589 local $errnum = Net::SSLeay::X509_STORE_CTX_get_error($_[1]); 3590 $verified_client = 1 if (!$errnum); 3591 } 3592return 1; 3593} 3594 3595sub END 3596{ 3597if ($doing_cgi_eval && $$ == $main_process_id) { 3598 # A CGI program called exit! This is a horrible hack to 3599 # finish up before really exiting 3600 shutdown(SOCK, 1); 3601 close(SOCK); 3602 close($PASSINw); close($PASSOUTw); 3603 &log_request($loghost, $authuser, $reqline, 3604 $cgiheader{"location"} ? "302" : $ok_code, &byte_count()); 3605 } 3606} 3607 3608# urlize 3609# Convert a string to a form ok for putting in a URL 3610sub urlize { 3611 local($tmp, $tmp2, $c); 3612 $tmp = $_[0]; 3613 $tmp2 = ""; 3614 while(($c = chop($tmp)) ne "") { 3615 if ($c !~ /[A-z0-9]/) { 3616 $c = sprintf("%%%2.2X", ord($c)); 3617 } 3618 $tmp2 = $c . $tmp2; 3619 } 3620 return $tmp2; 3621} 3622 3623# validate_user(username, password, host, remote-ip, webmin-port) 3624# Checks if some username and password are valid. Returns the modified username, 3625# the expired / temp pass flag, the non-existence flag, and the underlying 3626# Webmin username. 3627sub validate_user 3628{ 3629local ($user, $pass, $host, $actpip, $port) = @_; 3630return ( ) if (!$user); 3631print DEBUG "validate_user: user=$user pass=$pass host=$host\n"; 3632local ($canuser, $canmode, $notexist, $webminuser, $sudo) = 3633 &can_user_login($user, undef, $host); 3634print DEBUG "validate_user: canuser=$canuser canmode=$canmode notexist=$notexist webminuser=$webminuser sudo=$sudo\n"; 3635if ($notexist) { 3636 # User doesn't even exist, so go no further 3637 return ( undef, 0, 1, $webminuser ); 3638 } 3639elsif ($canmode == 0) { 3640 # User does exist but cannot login 3641 return ( $canuser, 0, 0, $webminuser ); 3642 } 3643elsif ($canmode == 1) { 3644 # Attempt Webmin authentication 3645 my $uinfo = &get_user_details($webminuser, $canuser); 3646 if ($uinfo && 3647 &password_crypt($pass, $uinfo->{'pass'}) eq $uinfo->{'pass'}) { 3648 # Password is valid .. but check for expiry 3649 local $lc = $uinfo->{'lastchanges'}; 3650 print DEBUG "validate_user: Password is valid lc=$lc pass_maxdays=$config{'pass_maxdays'}\n"; 3651 if ($config{'pass_maxdays'} && $lc && !$uinfo->{'nochange'}) { 3652 local $daysold = (time() - $lc)/(24*60*60); 3653 print DEBUG "maxdays=$config{'pass_maxdays'} daysold=$daysold temppass=$uinfo->{'temppass'}\n"; 3654 if ($config{'pass_lockdays'} && 3655 $daysold > $config{'pass_lockdays'}) { 3656 # So old that the account is locked 3657 return ( undef, 0, 0, $webminuser ); 3658 } 3659 elsif ($daysold > $config{'pass_maxdays'}) { 3660 # Password has expired 3661 return ( $user, 1, 0, $webminuser ); 3662 } 3663 } 3664 if ($uinfo->{'temppass'}) { 3665 # Temporary password - force change now 3666 return ( $user, 2, 0, $webminuser ); 3667 } 3668 return ( $user, 0, 0, $webminuser ); 3669 } 3670 elsif (!$uinfo) { 3671 print DEBUG "validate_user: User $webminuser not found\n"; 3672 return ( undef, 0, 0, $webminuser ); 3673 } 3674 else { 3675 print DEBUG "validate_user: User $webminuser password mismatch $pass != $uinfo->{'pass'}\n"; 3676 return ( undef, 0, 0, $webminuser ); 3677 } 3678 } 3679elsif ($canmode == 2 || $canmode == 3) { 3680 # Attempt PAM or passwd file authentication 3681 local $val = &validate_unix_user($canuser, $pass, $acptip, $port); 3682 print DEBUG "validate_user: unix val=$val\n"; 3683 if ($val && $sudo) { 3684 # Need to check if this Unix user can sudo 3685 if (!&check_sudo_permissions($canuser, $pass)) { 3686 print DEBUG "validate_user: sudo failed\n"; 3687 $val = 0; 3688 } 3689 else { 3690 print DEBUG "validate_user: sudo passed\n"; 3691 } 3692 } 3693 return $val == 2 ? ( $canuser, 1, 0, $webminuser ) : 3694 $val == 1 ? ( $canuser, 0, 0, $webminuser ) : 3695 ( undef, 0, 0, $webminuser ); 3696 } 3697elsif ($canmode == 4) { 3698 # Attempt external authentication 3699 return &validate_external_user($canuser, $pass) ? 3700 ( $canuser, 0, 0, $webminuser ) : 3701 ( undef, 0, 0, $webminuser ); 3702 } 3703else { 3704 # Can't happen! 3705 return ( ); 3706 } 3707} 3708 3709# validate_unix_user(user, password, remote-ip, local-port) 3710# Returns 1 if a username and password are valid under unix, 0 if not, 3711# or 2 if the account has expired. 3712# Checks PAM if available, and falls back to reading the system password 3713# file otherwise. 3714sub validate_unix_user 3715{ 3716if ($use_pam) { 3717 # Check with PAM 3718 $pam_username = $_[0]; 3719 $pam_password = $_[1]; 3720 eval "use Authen::PAM;"; 3721 local $pamh = new Authen::PAM($config{'pam'}, $pam_username, 3722 \&pam_conv_func); 3723 if (ref($pamh)) { 3724 $pamh->pam_set_item(PAM_RHOST(), $_[2]) if ($_[2]); 3725 $pamh->pam_set_item(PAM_TTY(), $_[3]) if ($_[3]); 3726 local $rcode = 0; 3727 local $pam_ret = $pamh->pam_authenticate(); 3728 if ($pam_ret == PAM_SUCCESS()) { 3729 # Logged in OK .. make sure password hasn't expired 3730 local $acct_ret = $pamh->pam_acct_mgmt(); 3731 $pam_ret = $acct_ret; 3732 if ($acct_ret == PAM_SUCCESS()) { 3733 $pamh->pam_open_session(); 3734 $rcode = 1; 3735 } 3736 elsif ($acct_ret == PAM_NEW_AUTHTOK_REQD() || 3737 $acct_ret == PAM_ACCT_EXPIRED()) { 3738 $rcode = 2; 3739 } 3740 else { 3741 print STDERR "Unknown pam_acct_mgmt return value : $acct_ret\n"; 3742 $rcode = 0; 3743 } 3744 } 3745 if ($config{'pam_end'}) { 3746 $pamh->pam_end($pam_ret); 3747 } 3748 return $rcode; 3749 } 3750 } 3751elsif ($config{'pam_only'}) { 3752 # Pam is not available, but configuration forces it's use! 3753 return 0; 3754 } 3755elsif ($config{'passwd_file'}) { 3756 # Check in a password file 3757 local $rv = 0; 3758 open(FILE, $config{'passwd_file'}); 3759 if ($config{'passwd_file'} eq '/etc/security/passwd') { 3760 # Assume in AIX format 3761 while(<FILE>) { 3762 s/\s*$//; 3763 if (/^\s*(\S+):/ && $1 eq $_[0]) { 3764 $_ = <FILE>; 3765 if (/^\s*password\s*=\s*(\S+)\s*$/) { 3766 $rv = $1 eq &password_crypt($_[1], $1) ? 3767 1 : 0; 3768 } 3769 last; 3770 } 3771 } 3772 } 3773 else { 3774 # Read the system password or shadow file 3775 while(<FILE>) { 3776 local @l = split(/:/, $_, -1); 3777 local $u = $l[$config{'passwd_uindex'}]; 3778 local $p = $l[$config{'passwd_pindex'}]; 3779 if ($u eq $_[0]) { 3780 $rv = $p eq &password_crypt($_[1], $p) ? 1 : 0; 3781 if ($config{'passwd_cindex'} ne '' && $rv) { 3782 # Password may have expired! 3783 local $c = $l[$config{'passwd_cindex'}]; 3784 local $m = $l[$config{'passwd_mindex'}]; 3785 local $day = time()/(24*60*60); 3786 if ($c =~ /^\d+/ && $m =~ /^\d+/ && 3787 $day - $c > $m) { 3788 # Yep, it has .. 3789 $rv = 2; 3790 } 3791 } 3792 if ($p eq "" && $config{'passwd_blank'}) { 3793 # Force password change 3794 $rv = 2; 3795 } 3796 last; 3797 } 3798 } 3799 } 3800 close(FILE); 3801 return $rv if ($rv); 3802 } 3803 3804# Fallback option - check password returned by getpw* 3805local @uinfo = getpwnam($_[0]); 3806if ($uinfo[1] ne '' && &password_crypt($_[1], $uinfo[1]) eq $uinfo[1]) { 3807 return 1; 3808 } 3809 3810return 0; # Totally failed 3811} 3812 3813# validate_external_user(user, pass) 3814# Validate a user by passing the username and password to an external 3815# squid-style authentication program 3816sub validate_external_user 3817{ 3818return 0 if (!$config{'extauth'}); 3819flock(EXTAUTH, 2); 3820local $str = "$_[0] $_[1]\n"; 3821syswrite(EXTAUTH, $str, length($str)); 3822local $resp = <EXTAUTH>; 3823flock(EXTAUTH, 8); 3824return $resp =~ /^OK/i ? 1 : 0; 3825} 3826 3827# can_user_login(username, no-append, host) 3828# Checks if a user can login or not. 3829# First return value is the username. 3830# Second is 0 if cannot login, 1 if using Webmin pass, 2 if PAM, 3 if password 3831# file, 4 if external. 3832# Third is 1 if the user does not exist at all, 0 if he does. 3833# Fourth is the Webmin username whose permissions apply, based on unixauth. 3834# Fifth is a flag indicating if a sudo check is needed. 3835sub can_user_login 3836{ 3837local $uinfo = &get_user_details($_[0]); 3838if (!$uinfo) { 3839 # See if this user exists in Unix and can be validated by the same 3840 # method as the unixauth webmin user 3841 local $realuser = $unixauth{$_[0]}; 3842 local @uinfo; 3843 local $sudo = 0; 3844 local $pamany = 0; 3845 eval { @uinfo = getpwnam($_[0]); }; # may fail on windows 3846 if (!$realuser && @uinfo) { 3847 # No unixauth entry for the username .. try his groups 3848 foreach my $ua (keys %unixauth) { 3849 if ($ua =~ /^\@(.*)$/) { 3850 if (&is_group_member(\@uinfo, $1)) { 3851 $realuser = $unixauth{$ua}; 3852 last; 3853 } 3854 } 3855 } 3856 } 3857 if (!$realuser && @uinfo) { 3858 # Fall back to unix auth for all Unix users 3859 $realuser = $unixauth{"*"}; 3860 } 3861 if (!$realuser && $use_sudo && @uinfo) { 3862 # Allow login effectively as root, if sudo permits it 3863 $sudo = 1; 3864 $realuser = "root"; 3865 } 3866 if (!$realuser && !@uinfo && $config{'pamany'}) { 3867 # If the user completely doesn't exist, we can still allow 3868 # him to authenticate via PAM 3869 $realuser = $config{'pamany'}; 3870 $pamany = 1; 3871 } 3872 if (!$realuser) { 3873 # For Usermin, always fall back to unix auth for any user, 3874 # so that later checks with domain added / removed are done. 3875 $realuser = $unixauth{"*"}; 3876 } 3877 return (undef, 0, 1, undef) if (!$realuser); 3878 local $uinfo = &get_user_details($realuser); 3879 return (undef, 0, 1, undef) if (!$uinfo); 3880 local $up = $uinfo->{'pass'}; 3881 3882 # Work out possible domain names from the hostname 3883 local @doms = ( $_[2] ); 3884 if ($_[2] =~ /^([^\.]+)\.(\S+)$/) { 3885 push(@doms, $2); 3886 } 3887 3888 if ($config{'user_mapping'} && !%user_mapping) { 3889 # Read the user mapping file 3890 %user_mapping = (); 3891 open(MAPPING, $config{'user_mapping'}); 3892 while(<MAPPING>) { 3893 s/\r|\n//g; 3894 s/#.*$//; 3895 if (/^(\S+)\s+(\S+)/) { 3896 if ($config{'user_mapping_reverse'}) { 3897 $user_mapping{$1} = $2; 3898 } 3899 else { 3900 $user_mapping{$2} = $1; 3901 } 3902 } 3903 } 3904 close(MAPPING); 3905 } 3906 3907 # Check the user mapping file to see if there is an entry for the 3908 # user login in which specifies a new effective user 3909 local $um; 3910 foreach my $d (@doms) { 3911 $um ||= $user_mapping{"$_[0]\@$d"}; 3912 } 3913 $um ||= $user_mapping{$_[0]}; 3914 if (defined($um) && ($_[1]&4) == 0) { 3915 # A mapping exists - use it! 3916 return &can_user_login($um, $_[1]+4, $_[2]); 3917 } 3918 3919 # Check if a user with the entered login and the domains appended 3920 # or prepended exists, and if so take it to be the effective user 3921 if (!@uinfo && $config{'domainuser'}) { 3922 # Try again with name.domain and name.firstpart 3923 local @firsts = map { /^([^\.]+)/; $1 } @doms; 3924 if (($_[1]&1) == 0) { 3925 local ($a, $p); 3926 foreach $a (@firsts, @doms) { 3927 foreach $p ("$_[0].${a}", "$_[0]-${a}", 3928 "${a}.$_[0]", "${a}-$_[0]", 3929 "$_[0]_${a}", "${a}_$_[0]") { 3930 local @vu = &can_user_login( 3931 $p, $_[1]+1, $_[2]); 3932 return @vu if ($vu[1]); 3933 } 3934 } 3935 } 3936 } 3937 3938 # Check if the user entered a domain at the end of his username when 3939 # he really shouldn't have, and if so try without it 3940 if (!@uinfo && $config{'domainstrip'} && 3941 $_[0] =~ /^(\S+)\@(\S+)$/ && ($_[1]&2) == 0) { 3942 local ($stripped, $dom) = ($1, $2); 3943 local @vu = &can_user_login($stripped, $_[1] + 2, $_[2]); 3944 return @vu if ($vu[1]); 3945 local @vu = &can_user_login($stripped, $_[1] + 2, $dom); 3946 return @vu if ($vu[1]); 3947 } 3948 3949 return ( undef, 0, 1, undef ) if (!@uinfo && !$pamany); 3950 3951 if (@uinfo) { 3952 if (scalar(@allowusers)) { 3953 # Only allow people on the allow list 3954 return ( undef, 0, 0, undef ) 3955 if (!&users_match(\@uinfo, @allowusers)); 3956 } 3957 elsif (scalar(@denyusers)) { 3958 # Disallow people on the deny list 3959 return ( undef, 0, 0, undef ) 3960 if (&users_match(\@uinfo, @denyusers)); 3961 } 3962 if ($config{'shells_deny'}) { 3963 local $found = 0; 3964 open(SHELLS, $config{'shells_deny'}); 3965 while(<SHELLS>) { 3966 s/\r|\n//g; 3967 s/#.*$//; 3968 $found++ if ($_ eq $uinfo[8]); 3969 } 3970 close(SHELLS); 3971 return ( undef, 0, 0, undef ) if (!$found); 3972 } 3973 } 3974 3975 if ($up eq 'x') { 3976 # PAM or passwd file authentication 3977 print DEBUG "can_user_login: Validate with PAM\n"; 3978 return ( $_[0], $use_pam ? 2 : 3, 0, $realuser, $sudo ); 3979 } 3980 elsif ($up eq 'e') { 3981 # External authentication 3982 print DEBUG "can_user_login: Validate externally\n"; 3983 return ( $_[0], 4, 0, $realuser, $sudo ); 3984 } 3985 else { 3986 # Fixed Webmin password 3987 print DEBUG "can_user_login: Validate by Webmin\n"; 3988 return ( $_[0], 1, 0, $realuser, $sudo ); 3989 } 3990 } 3991elsif ($uinfo->{'pass'} eq 'x') { 3992 # Webmin user authenticated via PAM or password file 3993 return ( $_[0], $use_pam ? 2 : 3, 0, $_[0] ); 3994 } 3995elsif ($uinfo->{'pass'} eq 'e') { 3996 # Webmin user authenticated externally 3997 return ( $_[0], 4, 0, $_[0] ); 3998 } 3999else { 4000 # Normal Webmin user 4001 return ( $_[0], 1, 0, $_[0] ); 4002 } 4003} 4004 4005# the PAM conversation function for interactive logins 4006sub pam_conv_func 4007{ 4008$pam_conv_func_called++; 4009my @res; 4010while ( @_ ) { 4011 my $code = shift; 4012 my $msg = shift; 4013 my $ans = ""; 4014 4015 $ans = $pam_username if ($code == PAM_PROMPT_ECHO_ON() ); 4016 $ans = $pam_password if ($code == PAM_PROMPT_ECHO_OFF() ); 4017 4018 push @res, PAM_SUCCESS(); 4019 push @res, $ans; 4020 } 4021push @res, PAM_SUCCESS(); 4022return @res; 4023} 4024 4025sub urandom_timeout 4026{ 4027close(RANDOM); 4028} 4029 4030# get_socket_ip(handle, ipv6-flag) 4031# Returns the local IP address of some connection, as both a string and in 4032# binary format 4033sub get_socket_ip 4034{ 4035local ($fh, $ipv6) = @_; 4036local $sn = getsockname($fh); 4037return undef if (!$sn); 4038return &get_address_ip($sn, $ipv6); 4039} 4040 4041# get_address_ip(address, ipv6-flag) 4042# Given a sockaddr object in binary format, return the binary address, text 4043# address and port number 4044sub get_address_ip 4045{ 4046local ($sn, $ipv6) = @_; 4047if ($ipv6) { 4048 local ($p, $b) = unpack_sockaddr_in6($sn); 4049 return ($b, inet_ntop(AF_INET6(), $b), $p); 4050 } 4051else { 4052 local ($p, $b) = unpack_sockaddr_in($sn); 4053 return ($b, inet_ntoa($b), $p); 4054 } 4055} 4056 4057# get_socket_name(handle, ipv6-flag) 4058# Returns the local hostname or IP address of some connection 4059sub get_socket_name 4060{ 4061local ($fh, $ipv6) = @_; 4062return $config{'host'} if ($config{'host'}); 4063local ($mybin, $myaddr) = &get_socket_ip($fh, $ipv6); 4064if (!$get_socket_name_cache{$myaddr}) { 4065 local $myname; 4066 if (!$config{'no_resolv_myname'}) { 4067 $myname = gethostbyaddr($mybin, 4068 $ipv6 ? AF_INET6() : AF_INET); 4069 } 4070 $myname ||= $myaddr; 4071 $get_socket_name_cache{$myaddr} = $myname; 4072 } 4073return $get_socket_name_cache{$myaddr}; 4074} 4075 4076# run_login_script(username, sid, remoteip, localip) 4077sub run_login_script 4078{ 4079if ($config{'login_script'}) { 4080 alarm(5); 4081 $SIG{'ALRM'} = sub { die "timeout" }; 4082 eval { 4083 system($config{'login_script'}. 4084 " ".join(" ", map { quotemeta($_) || '""' } @_). 4085 " >/dev/null 2>&1 </dev/null"); 4086 }; 4087 alarm(0); 4088 } 4089} 4090 4091# run_logout_script(username, sid, remoteip, localip) 4092sub run_logout_script 4093{ 4094if ($config{'logout_script'}) { 4095 alarm(5); 4096 $SIG{'ALRM'} = sub { die "timeout" }; 4097 eval { 4098 system($config{'logout_script'}. 4099 " ".join(" ", map { quotemeta($_) || '""' } @_). 4100 " >/dev/null 2>&1 </dev/null"); 4101 }; 4102 alarm(0); 4103 } 4104} 4105 4106# run_failed_script(username, reason-code, remoteip, localip) 4107sub run_failed_script 4108{ 4109if ($config{'failed_script'}) { 4110 $_[0] =~ s/\r|\n/ /g; 4111 alarm(5); 4112 $SIG{'ALRM'} = sub { die "timeout" }; 4113 eval { 4114 system($config{'failed_script'}. 4115 " ".join(" ", map { quotemeta($_) || '""' } @_). 4116 " >/dev/null 2>&1 </dev/null"); 4117 }; 4118 alarm(0); 4119 } 4120} 4121 4122# close_all_sockets() 4123# Closes all the main listening sockets 4124sub close_all_sockets 4125{ 4126local $s; 4127foreach $s (@socketfhs) { 4128 close($s); 4129 } 4130} 4131 4132# close_all_pipes() 4133# Close all pipes for talking to sub-processes 4134sub close_all_pipes 4135{ 4136local $p; 4137foreach $p (@passin) { close($p); } 4138foreach $p (@passout) { close($p); } 4139foreach $p (values %conversations) { 4140 if ($p->{'PAMOUTr'}) { 4141 close($p->{'PAMOUTr'}); 4142 close($p->{'PAMINw'}); 4143 } 4144 } 4145} 4146 4147# check_user_ip(user) 4148# Returns 1 if some user is allowed to login from the accepting IP, 0 if not 4149sub check_user_ip 4150{ 4151local ($username) = @_; 4152local $uinfo = &get_user_details($username); 4153return 1 if (!$uinfo); 4154if ($uinfo->{'deny'} && 4155 &ip_match($acptip, $localip, @{$uinfo->{'deny'}}) || 4156 $uinfo->{'allow'} && 4157 !&ip_match($acptip, $localip, @{$uinfo->{'allow'}})) { 4158 return 0; 4159 } 4160return 1; 4161} 4162 4163# check_user_time(user) 4164# Returns 1 if some user is allowed to login at the current date and time 4165sub check_user_time 4166{ 4167local ($username) = @_; 4168local $uinfo = &get_user_details($username); 4169return 1 if (!$uinfo || !$uinfo->{'allowdays'} && !$uinfo->{'allowhours'}); 4170local @tm = localtime(time()); 4171if ($uinfo->{'allowdays'}) { 4172 # Make sure day is allowed 4173 return 0 if (&indexof($tm[6], @{$uinfo->{'allowdays'}}) < 0); 4174 } 4175if ($uinfo->{'allowhours'}) { 4176 # Make sure time is allowed 4177 local $m = $tm[2]*60+$tm[1]; 4178 return 0 if ($m < $uinfo->{'allowhours'}->[0] || 4179 $m > $uinfo->{'allowhours'}->[1]); 4180 } 4181return 1; 4182} 4183 4184# generate_random_id(password, [force-urandom]) 4185# Returns a random session ID number 4186sub generate_random_id 4187{ 4188my ($force_urandom) = @_; 4189local $sid; 4190if (!$bad_urandom) { 4191 # First try /dev/urandom, unless we have marked it as bad 4192 $SIG{ALRM} = "miniserv::urandom_timeout"; 4193 alarm(5); 4194 if (open(RANDOM, "/dev/urandom")) { 4195 my $tmpsid; 4196 if (read(RANDOM, $tmpsid, 16) == 16) { 4197 $sid = lc(unpack('h*',$tmpsid)); 4198 if ($sid !~ /^[0-9a-fA-F]{32}$/) { 4199 $sid = 'bad'; 4200 } 4201 } 4202 close(RANDOM); 4203 } 4204 alarm(0); 4205 } 4206if (!$sid && !$force_urandom) { 4207 my $offset = int(rand(2048)); 4208 my @charset = ('0' ..'9', 'a' .. 'f'); 4209 $sid = join('', map { $charset[rand(@charset)] } 1 .. 4096); 4210 $sid = substr($sid, $offset, 32); 4211 } 4212return $sid; 4213} 4214 4215# handle_login(username, ok, expired, not-exists, password, [no-test-cookie], [no-log]) 4216# Called from handle_session to either mark a user as logged in, or not 4217sub handle_login 4218{ 4219local ($vu, $ok, $expired, $nonexist, $pass, $notest, $nolog) = @_; 4220$authuser = $vu if ($ok); 4221 4222# check if the test cookie is set 4223if ($header{'cookie'} !~ /testing=1/ && $vu && 4224 !$config{'no_testing_cookie'} && !$notest) { 4225 &http_error(500, "Cache issue or no cookies support", 4226 "Please clear your browser's cache for the given ". 4227 "domain and/or try incognito tab; double check ". 4228 "to have cookies support enabled."); 4229 } 4230 4231# check with main process for delay 4232if ($config{'passdelay'} && $vu) { 4233 print DEBUG "handle_login: requesting delay vu=$vu acptip=$acptip ok=$ok\n"; 4234 print $PASSINw "delay $vu $acptip $ok $nolog\n"; 4235 <$PASSOUTr> =~ /(\d+) (\d+)/; 4236 $blocked = $2; 4237 sleep($1); 4238 print DEBUG "handle_login: delay=$1 blocked=$2\n"; 4239 } 4240 4241if ($ok && (!$expired || 4242 $config{'passwd_mode'} == 1)) { 4243 # Logged in OK! Tell the main process about 4244 # the new SID 4245 local $sid = &generate_random_id(); 4246 print DEBUG "handle_login: sid=$sid\n"; 4247 print $PASSINw "new $sid $authuser $acptip\n"; 4248 4249 # Run the post-login script, if any 4250 &run_login_script($authuser, $sid, 4251 $loghost, $localip); 4252 4253 # Check for a redirect URL for the user 4254 local $rurl = &login_redirect($authuser, $pass, $host); 4255 print DEBUG "handle_login: redirect URL rurl=$rurl\n"; 4256 if ($rurl) { 4257 # Got one .. go to it 4258 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 4259 &write_data("Date: $datestr\r\n"); 4260 &write_data("Server: $config{'server'}\r\n"); 4261 &write_data("Location: $rurl\r\n"); 4262 &write_keep_alive(0); 4263 &write_data("\r\n"); 4264 &log_request($loghost, $authuser, $reqline, 302, 0); 4265 } 4266 else { 4267 # Set cookie and redirect to originally requested page 4268 &write_data("HTTP/1.0 302 Moved Temporarily\r\n"); 4269 &write_data("Date: $datestr\r\n"); 4270 &write_data("Server: $config{'server'}\r\n"); 4271 local $sec = $ssl ? "; secure" : ""; 4272 if (!$config{'no_httponly'}) { 4273 $sec .= "; httpOnly"; 4274 } 4275 if ($in{'page'} !~ /^\/[A-Za-z0-9\/\.\-\_:]+$/) { 4276 # Make redirect URL safe 4277 $in{'page'} = "/"; 4278 } 4279 local $cpath = $config{'cookiepath'}; 4280 if ($in{'save'}) { 4281 &write_data("Set-Cookie: $sidname=$sid; path=$cpath; ". 4282 "expires=\"Thu, 31-Dec-2037 00:00:00\"$sec\r\n"); 4283 } 4284 else { 4285 &write_data("Set-Cookie: $sidname=$sid; path=$cpath". 4286 "$sec\r\n"); 4287 } 4288 &write_data("Location: $prot://$hostport$in{'page'}\r\n"); 4289 &write_keep_alive(0); 4290 &write_data("\r\n"); 4291 &log_request($loghost, $authuser, $reqline, 302, 0); 4292 syslog("info", "%s", "Successful login as $authuser from $loghost") if ($use_syslog); 4293 &write_login_utmp($authuser, $acpthost); 4294 } 4295 return 0; 4296 } 4297elsif ($ok && $expired && 4298 ($config{'passwd_mode'} == 2 || $expired == 2)) { 4299 # Login was ok, but password has expired or was temporary. Need 4300 # to force display of password change form. 4301 &run_failed_script($authuser, 'expiredpass', 4302 $loghost, $localip); 4303 $validated = 1; 4304 $authuser = undef; 4305 $querystring = "&user=".&urlize($vu). 4306 "&pam=".$use_pam. 4307 "&expired=".$expired; 4308 $method = "GET"; 4309 $queryargs = ""; 4310 $page = $config{'password_form'}; 4311 $logged_code = 401; 4312 $miniserv_internal = 2; 4313 syslog("crit", "%s", 4314 "Expired login as $vu ". 4315 "from $loghost") if ($use_syslog); 4316 } 4317else { 4318 # Login failed, or password has expired. The login form will be 4319 # displayed again by later code 4320 &run_failed_script($vu, $handle_login ? 'wronguser' : 4321 $expired ? 'expiredpass' : 'wrongpass', 4322 $loghost, $localip); 4323 $failed_user = $vu; 4324 $failed_pass = $pass; 4325 $failed_save = $in{'save'}; 4326 $failed_twofactor_attempt = $in{'failed_twofactor_attempt'} || 0; 4327 $failed_twofactor_attempt++; 4328 $request_uri = $in{'page'}; 4329 $already_session_id = undef; 4330 $method = "GET"; 4331 $authuser = $baseauthuser = undef; 4332 4333 # If login page is simply reloaded, with `session_login.cgi` in URL, 4334 # without having any parameters sent (user set to empty), don't log 4335 # false positive attempt with `Invalid login as from IP` to syslog 4336 $nolog = 1 if (!$vu); 4337 4338 # Send to log if allowed 4339 syslog("crit", "%s", 4340 ($nonexist ? "Non-existent" : 4341 $expired ? "Expired" : "Invalid"). 4342 " login as $vu from $loghost") 4343 if ($use_syslog && !$nolog); 4344 } 4345return undef; 4346} 4347 4348# write_login_utmp(user, host) 4349# Record the login by some user in utmp 4350sub write_login_utmp 4351{ 4352if ($write_utmp) { 4353 # Write utmp record for login 4354 %utmp = ( 'ut_host' => $_[1], 4355 'ut_time' => time(), 4356 'ut_user' => $_[0], 4357 'ut_type' => 7, # user process 4358 'ut_pid' => $miniserv_main_pid, 4359 'ut_line' => $config{'pam'}, 4360 'ut_id' => '' ); 4361 if (defined(&User::Utmp::putut)) { 4362 User::Utmp::putut(\%utmp); 4363 } 4364 else { 4365 User::Utmp::pututline(\%utmp); 4366 } 4367 } 4368} 4369 4370# write_logout_utmp(user, host) 4371# Record the logout by some user in utmp 4372sub write_logout_utmp 4373{ 4374if ($write_utmp) { 4375 # Write utmp record for logout 4376 %utmp = ( 'ut_host' => $_[1], 4377 'ut_time' => time(), 4378 'ut_user' => $_[0], 4379 'ut_type' => 8, # dead process 4380 'ut_pid' => $miniserv_main_pid, 4381 'ut_line' => $config{'pam'}, 4382 'ut_id' => '' ); 4383 if (defined(&User::Utmp::putut)) { 4384 User::Utmp::putut(\%utmp); 4385 } 4386 else { 4387 User::Utmp::pututline(\%utmp); 4388 } 4389 } 4390} 4391 4392# pam_conversation_process(username, write-pipe, read-pipe) 4393# This function is called inside a sub-process to communicate with PAM. It sends 4394# questions down one pipe, and reads responses from another 4395sub pam_conversation_process 4396{ 4397local ($user, $writer, $reader) = @_; 4398$miniserv::pam_conversation_process_writer = $writer; 4399$miniserv::pam_conversation_process_reader = $reader; 4400eval "use Authen::PAM;"; 4401local $convh = new Authen::PAM( 4402 $config{'pam'}, $user, \&miniserv::pam_conversation_process_func); 4403local $pam_ret = $convh->pam_authenticate(); 4404if ($pam_ret == PAM_SUCCESS()) { 4405 local $acct_ret = $convh->pam_acct_mgmt(); 4406 if ($acct_ret == PAM_SUCCESS()) { 4407 $convh->pam_open_session(); 4408 print $writer "x2 $user 1 0 0\n"; 4409 } 4410 elsif ($acct_ret == PAM_NEW_AUTHTOK_REQD() || 4411 $acct_ret == PAM_ACCT_EXPIRED()) { 4412 print $writer "x2 $user 1 1 0\n"; 4413 } 4414 else { 4415 print $writer "x0 Unknown PAM account status $acct_ret\n"; 4416 } 4417 } 4418else { 4419 print $writer "x2 $user 0 0 0\n"; 4420 } 4421exit(0); 4422} 4423 4424# pam_conversation_process_func(type, message, [type, message, ...]) 4425# A pipe that talks to both PAM and the master process 4426sub pam_conversation_process_func 4427{ 4428local @rv; 4429select($miniserv::pam_conversation_process_writer); $| = 1; select(STDOUT); 4430while(@_) { 4431 local ($type, $msg) = (shift, shift); 4432 $msg =~ s/\r|\n//g; 4433 local $ok = (print $miniserv::pam_conversation_process_writer "$type $msg\n"); 4434 print $miniserv::pam_conversation_process_writer "\n"; 4435 local $answer = <$miniserv::pam_conversation_process_reader>; 4436 $answer =~ s/\r|\n//g; 4437 push(@rv, PAM_SUCCESS(), $answer); 4438 } 4439push(@rv, PAM_SUCCESS()); 4440return @rv; 4441} 4442 4443# allocate_pipes() 4444# Returns 4 new pipe file handles 4445sub allocate_pipes 4446{ 4447local ($PASSINr, $PASSINw, $PASSOUTr, $PASSOUTw); 4448local $p; 4449local %taken = ( (map { $_, 1 } @passin), 4450 (map { $_->{'PASSINr'} } values %conversations) ); 4451for($p=0; $taken{"PASSINr$p"}; $p++) { } 4452$PASSINr = "PASSINr$p"; 4453$PASSINw = "PASSINw$p"; 4454$PASSOUTr = "PASSOUTr$p"; 4455$PASSOUTw = "PASSOUTw$p"; 4456pipe($PASSINr, $PASSINw); 4457pipe($PASSOUTr, $PASSOUTw); 4458select($PASSINw); $| = 1; 4459select($PASSINr); $| = 1; 4460select($PASSOUTw); $| = 1; 4461select($PASSOUTw); $| = 1; 4462select(STDOUT); 4463return ($PASSINr, $PASSINw, $PASSOUTr, $PASSOUTw); 4464} 4465 4466# recv_pam_question(&conv, fd) 4467# Reads one PAM question from the sub-process, and sends it to the HTTP handler. 4468# Returns 0 if the conversation is over, 1 if not. 4469sub recv_pam_question 4470{ 4471local ($conf, $fh) = @_; 4472local $pr = $conf->{'PAMOUTr'}; 4473select($pr); $| = 1; select(STDOUT); 4474local $line = <$pr>; 4475$line =~ s/\r|\n//g; 4476if (!$line) { 4477 $line = <$pr>; 4478 $line =~ s/\r|\n//g; 4479 } 4480$conf->{'last'} = time(); 4481if (!$line) { 4482 # Failed! 4483 print $fh "0 PAM conversation error\n"; 4484 return 0; 4485 } 4486else { 4487 local ($type, $msg) = split(/\s+/, $line, 2); 4488 if ($type =~ /^x(\d+)/) { 4489 # Pass this status code through 4490 print $fh "$1 $msg\n"; 4491 return $1 == 2 || $1 == 0 ? 0 : 1; 4492 } 4493 elsif ($type == PAM_PROMPT_ECHO_ON()) { 4494 # A normal question 4495 print $fh "1 $msg\n"; 4496 return 1; 4497 } 4498 elsif ($type == PAM_PROMPT_ECHO_OFF()) { 4499 # A password 4500 print $fh "3 $msg\n"; 4501 return 1; 4502 } 4503 elsif ($type == PAM_ERROR_MSG() || $type == PAM_TEXT_INFO()) { 4504 # A message that does not require a response 4505 print $fh "4 $msg\n"; 4506 return 1; 4507 } 4508 else { 4509 # Unknown type! 4510 print $fh "0 Unknown PAM message type $type\n"; 4511 return 0; 4512 } 4513 } 4514} 4515 4516# send_pam_answer(&conv, answer) 4517# Sends a response from the user to the PAM sub-process 4518sub send_pam_answer 4519{ 4520local ($conf, $answer) = @_; 4521local $pw = $conf->{'PAMINw'}; 4522$conf->{'last'} = time(); 4523print $pw "$answer\n"; 4524} 4525 4526# end_pam_conversation(&conv) 4527# Clean up PAM conversation pipes and processes 4528sub end_pam_conversation 4529{ 4530local ($conv) = @_; 4531kill('KILL', $conv->{'pid'}) if ($conv->{'pid'}); 4532if ($conv->{'PAMINr'}) { 4533 close($conv->{'PAMINr'}); 4534 close($conv->{'PAMOUTr'}); 4535 close($conv->{'PAMINw'}); 4536 close($conv->{'PAMOUTw'}); 4537 } 4538delete($conversations{$conv->{'cid'}}); 4539} 4540 4541# get_ipkeys(&miniserv) 4542# Returns a list of IP address to key file mappings from a miniserv.conf entry 4543sub get_ipkeys 4544{ 4545local (@rv, $k); 4546foreach $k (keys %{$_[0]}) { 4547 if ($k =~ /^ipkey_(\S+)/) { 4548 local $ipkey = { 'ips' => [ split(/,/, $1) ], 4549 'key' => $_[0]->{$k}, 4550 'index' => scalar(@rv) }; 4551 $ipkey->{'cert'} = $_[0]->{'ipcert_'.$1}; 4552 $ipkey->{'extracas'} = $_[0]->{'ipextracas_'.$1}; 4553 push(@rv, $ipkey); 4554 } 4555 } 4556return @rv; 4557} 4558 4559# create_ssl_context(keyfile, [certfile], [extracas]) 4560sub create_ssl_context 4561{ 4562local ($keyfile, $certfile, $extracas) = @_; 4563local $ssl_ctx; 4564eval { $ssl_ctx = Net::SSLeay::new_x_ctx() }; 4565$ssl_ctx ||= Net::SSLeay::CTX_new(); 4566$ssl_ctx || die "Failed to create SSL context : $!"; 4567my @extracas = $extracas && $extracas ne "none" ? split(/\s+/, $extracas) : (); 4568 4569# Validate cert files 4570if (!-r $keyfile) { 4571 print STDERR "SSL key file $keyfile does not exist\n"; 4572 return undef; 4573 } 4574if ($certfile && !-r $certfile) { 4575 print STDERR "SSL cert file $certfile does not exist\n"; 4576 return undef; 4577 } 4578foreach my $p (@extracas) { 4579 if (!-r $p) { 4580 print STDERR "SSL CA file $p does not exist\n"; 4581 return undef; 4582 } 4583 } 4584 4585# Setup PFS, if ciphers are in use 4586if (-r $config{'dhparams_file'}) { 4587 eval { 4588 my $bio = Net::SSLeay::BIO_new_file( 4589 $config{'dhparams_file'}, 'r'); 4590 my $DHP = Net::SSLeay::PEM_read_bio_DHparams($bio); 4591 Net::SSLeay::CTX_set_tmp_dh($ssl_ctx, $DHP); 4592 my $nid = Net::SSLeay::OBJ_sn2nid("secp384r1"); 4593 my $curve = Net::SSLeay::EC_KEY_new_by_curve_name($nid); 4594 Net::SSLeay::CTX_set_tmp_ecdh($ssl_ctx, $curve); 4595 Net::SSLeay::BIO_free($bio); 4596 }; 4597 } 4598if ($@) { 4599 print STDERR "Failed to load $config{'dhparams_file'} : $@\n"; 4600 } 4601 4602if ($client_certs) { 4603 Net::SSLeay::CTX_load_verify_locations( 4604 $ssl_ctx, $config{'ca'}, ""); 4605 eval { 4606 Net::SSLeay::set_verify( 4607 $ssl_ctx, &Net::SSLeay::VERIFY_PEER, \&verify_client); 4608 }; 4609 if ($@) { 4610 Net::SSLeay::CTX_set_verify( 4611 $ssl_ctx, &Net::SSLeay::VERIFY_PEER, \&verify_client); 4612 } 4613 } 4614foreach my $p (@extracas) { 4615 Net::SSLeay::CTX_load_verify_locations($ssl_ctx, $p, ""); 4616 } 4617 4618if (!Net::SSLeay::CTX_use_PrivateKey_file($ssl_ctx, $keyfile, 4619 &Net::SSLeay::FILETYPE_PEM)) { 4620 print STDERR "Failed to open SSL key $keyfile\n"; 4621 return undef; 4622 } 4623if (!Net::SSLeay::CTX_use_certificate_file($ssl_ctx, $certfile || $keyfile, 4624 &Net::SSLeay::FILETYPE_PEM)) { 4625 print STDERR "Failed to open SSL cert ".($certfile || $keyfile)."\n"; 4626 return undef; 4627 } 4628 4629if ($config{'no_ssl2'}) { 4630 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4631 &Net::SSLeay::OP_NO_SSLv2)'; 4632 } 4633if ($config{'no_ssl3'}) { 4634 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4635 &Net::SSLeay::OP_NO_SSLv3)'; 4636 } 4637if ($config{'no_tls1'}) { 4638 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4639 &Net::SSLeay::OP_NO_TLSv1)'; 4640 } 4641if ($config{'no_tls1_1'}) { 4642 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4643 &Net::SSLeay::OP_NO_TLSv1_1)'; 4644 } 4645if ($config{'no_tls1_2'}) { 4646 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4647 &Net::SSLeay::OP_NO_TLSv1_2)'; 4648 } 4649if ($config{'no_sslcompression'}) { 4650 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4651 &Net::SSLeay::OP_NO_COMPRESSION)'; 4652 } 4653if ($config{'ssl_honorcipherorder'}) { 4654 eval 'Net::SSLeay::CTX_set_options($ssl_ctx, 4655 &Net::SSLeay::OP_CIPHER_SERVER_PREFERENCE)'; 4656 } 4657 4658return $ssl_ctx; 4659} 4660 4661# ssl_connection_for_ip(socket, ipv6-flag) 4662# Returns a new SSL connection object for some socket, or undef if failed 4663sub ssl_connection_for_ip 4664{ 4665local ($sock, $ipv6) = @_; 4666local $sn = getsockname($sock); 4667if (!$sn) { 4668 print STDERR "Failed to get address for socket $sock\n"; 4669 return undef; 4670 } 4671local (undef, $myip, undef) = &get_address_ip($sn, $ipv6); 4672local $ssl_ctx = $ssl_contexts{$myip} || $ssl_contexts{"*"}; 4673local $ssl_con = Net::SSLeay::new($ssl_ctx); 4674if ($config{'ssl_cipher_list'}) { 4675 # Force use of ciphers 4676 eval "Net::SSLeay::set_cipher_list( 4677 \$ssl_con, \$config{'ssl_cipher_list'})"; 4678 if ($@) { 4679 print STDERR "SSL cipher $config{'ssl_cipher_list'} failed : ", 4680 "$@\n"; 4681 } 4682 } 4683Net::SSLeay::set_fd($ssl_con, fileno($sock)); 4684if (!Net::SSLeay::accept($ssl_con)) { 4685 return undef; 4686 } 4687return $ssl_con; 4688} 4689 4690# login_redirect(username, password, host) 4691# Calls the login redirect script (if configured), which may output a URL to 4692# re-direct a user to after logging in. 4693sub login_redirect 4694{ 4695return undef if (!$config{'login_redirect'}); 4696local $quser = quotemeta($_[0]); 4697local $qpass = quotemeta($_[1]); 4698local $qhost = quotemeta($_[2]); 4699local $url = `$config{'login_redirect'} $quser $qpass $qhost`; 4700chop($url); 4701return $url; 4702} 4703 4704# reload_config_file() 4705# Re-read %config, and call post-config actions 4706sub reload_config_file 4707{ 4708&log_error("Reloading configuration"); 4709%config = &read_config_file($config_file); 4710&update_vital_config(); 4711&read_users_file(); 4712&read_mime_types(); 4713&build_config_mappings(); 4714&read_webmin_crons(); 4715&precache_files(); 4716if ($config{'session'}) { 4717 dbmclose(%sessiondb); 4718 dbmopen(%sessiondb, $config{'sessiondb'}, 0700); 4719 } 4720} 4721 4722# read_config_file(file) 4723# Reads the given config file, and returns a hash of values 4724sub read_config_file 4725{ 4726local %rv; 4727open(CONF, $_[0]) || die "Failed to open config file $_[0] : $!"; 4728while(<CONF>) { 4729 s/\r|\n//g; 4730 if (/^#/ || !/\S/) { next; } 4731 /^([^=]+)=(.*)$/; 4732 $name = $1; $val = $2; 4733 $name =~ s/^\s+//g; $name =~ s/\s+$//g; 4734 $val =~ s/^\s+//g; $val =~ s/\s+$//g; 4735 $rv{$name} = $val; 4736 } 4737close(CONF); 4738return %rv; 4739} 4740 4741# read_any_file(file) 4742# Reads any given file and returns its content 4743sub read_any_file 4744{ 4745my ($realfile) = @_; 4746my $rv; 4747open(my $fh, "<".$realfile) || return $rv; 4748local $/; 4749$rv = <$fh>; 4750close($fh); 4751return $rv; 4752} 4753 4754# update_vital_config() 4755# Updates %config with defaults, and dies if something vital is missing 4756sub update_vital_config 4757{ 4758my %vital = ("port", 80, 4759 "root", "./", 4760 "server", "MiniServ/0.01", 4761 "index_docs", "index.html index.htm index.cgi index.php", 4762 "addtype_html", "text/html", 4763 "addtype_txt", "text/plain", 4764 "addtype_gif", "image/gif", 4765 "addtype_jpg", "image/jpeg", 4766 "addtype_jpeg", "image/jpeg", 4767 "realm", "MiniServ", 4768 "session_login", "/session_login.cgi", 4769 "pam_login", "/pam_login.cgi", 4770 "password_form", "/password_form.cgi", 4771 "password_change", "/password_change.cgi", 4772 "maxconns", 50, 4773 "maxconns_per_ip", 25, 4774 "maxconns_per_net", 35, 4775 "pam", "webmin", 4776 "sidname", "sid", 4777 "unauth", "^/unauthenticated/ ^/robots.txt\$ ^[A-Za-z0-9\\-/_]+\\.jar\$ ^[A-Za-z0-9\\-/_]+\\.class\$ ^[A-Za-z0-9\\-/_]+\\.gif\$ ^[A-Za-z0-9\\-/_]+\\.png\$ ^[A-Za-z0-9\\-/_]+\\.conf\$ ^[A-Za-z0-9\\-/_]+\\.ico\$ ^/robots.txt\$", 4778 "max_post", 10000, 4779 "expires", 7*24*60*60, 4780 "pam_test_user", "root", 4781 "precache", "lang/en */lang/en", 4782 "cookiepath", "/", 4783 ); 4784foreach my $v (keys %vital) { 4785 if (!$config{$v}) { 4786 if ($vital{$v} eq "") { 4787 die "Missing config option $v"; 4788 } 4789 $config{$v} = $vital{$v}; 4790 } 4791 } 4792$config_file =~ /^(.*)\/[^\/]+$/; 4793my $config_dir = $1; 4794$config{'pidfile'} =~ /^(.*)\/[^\/]+$/; 4795my $var_dir = $1; 4796if (!$config{'sessiondb'}) { 4797 $config{'sessiondb'} = "$var_dir/sessiondb"; 4798 } 4799if (!$config{'errorlog'}) { 4800 $config{'logfile'} =~ /^(.*)\/[^\/]+$/; 4801 $config{'errorlog'} = "$1/miniserv.error"; 4802 } 4803if (!$config{'tempbase'}) { 4804 $config{'tempbase'} = "$var_dir/cgitemp"; 4805 } 4806if (!$config{'blockedfile'}) { 4807 $config{'blockedfile'} = "$var_dir/blocked"; 4808 } 4809if (!$config{'webmincron_dir'}) { 4810 $config{'webmincron_dir'} = "$config_dir/webmincron/crons"; 4811 } 4812if (!$config{'webmincron_last'}) { 4813 $config{'logfile'} =~ /^(.*)\/[^\/]+$/; 4814 $config{'webmincron_last'} = "$1/miniserv.lastcrons"; 4815 } 4816if (!$config{'webmincron_wrapper'}) { 4817 $config{'webmincron_wrapper'} = $config{'root'}. 4818 "/webmincron/webmincron.pl"; 4819 } 4820if (!$config{'twofactor_wrapper'}) { 4821 $config{'twofactor_wrapper'} = $config{'root'}."/acl/twofactor.pl"; 4822 } 4823$config{'restartflag'} ||= $var_dir."/restart-flag"; 4824$config{'reloadflag'} ||= $var_dir."/reload-flag"; 4825$config{'stopflag'} ||= $var_dir."/stop-flag"; 4826} 4827 4828# read_users_file() 4829# Fills the %users and %certs hashes from the users file in %config 4830sub read_users_file 4831{ 4832undef(%users); 4833undef(%certs); 4834undef(%allow); 4835undef(%deny); 4836undef(%allowdays); 4837undef(%allowhours); 4838undef(%lastchanges); 4839undef(%nochange); 4840undef(%temppass); 4841undef(%twofactor); 4842if ($config{'userfile'}) { 4843 open(USERS, $config{'userfile'}); 4844 while(<USERS>) { 4845 s/\r|\n//g; 4846 local @user = split(/:/, $_, -1); 4847 $users{$user[0]} = $user[1]; 4848 $certs{$user[0]} = $user[3] if ($user[3]); 4849 if ($user[4] =~ /^allow\s+(.*)/) { 4850 my $allow = $1; 4851 $allow =~ s/;/:/g; 4852 $allow{$user[0]} = $config{'alwaysresolve'} ? 4853 [ split(/\s+/, $allow) ] : 4854 [ &to_ip46address(split(/\s+/, $allow)) ]; 4855 } 4856 elsif ($user[4] =~ /^deny\s+(.*)/) { 4857 my $deny = $1; 4858 $deny =~ s/;/:/g; 4859 $deny{$user[0]} = $config{'alwaysresolve'} ? 4860 [ split(/\s+/, $deny) ] : 4861 [ &to_ip46address(split(/\s+/, $deny)) ]; 4862 } 4863 if ($user[5] =~ /days\s+(\S+)/) { 4864 $allowdays{$user[0]} = [ split(/,/, $1) ]; 4865 } 4866 if ($user[5] =~ /hours\s+(\d+)\.(\d+)-(\d+).(\d+)/) { 4867 $allowhours{$user[0]} = [ $1*60+$2, $3*60+$4 ]; 4868 } 4869 $lastchanges{$user[0]} = $user[6]; 4870 $nochange{$user[0]} = $user[9]; 4871 $temppass{$user[0]} = $user[10]; 4872 if ($user[11] && $user[12]) { 4873 $twofactor{$user[0]} = { 'provider' => $user[11], 4874 'id' => $user[12], 4875 'apikey' => $user[13] }; 4876 } 4877 } 4878 close(USERS); 4879 } 4880if ($config{'twofactorfile'}) { 4881 open(TWO, $config{'twofactorfile'}); 4882 while(<TWO>) { 4883 s/\r|\n//g; 4884 local @two = split(/:/, $_, -1); 4885 $twofactor{$two[0]} = { 'provider' => $two[1], 4886 'id' => $two[2], 4887 'apikey' => $two[3], }; 4888 } 4889 close(TWO); 4890 } 4891 4892# Test user DB, if configured 4893if ($config{'userdb'}) { 4894 my $dbh = &connect_userdb($config{'userdb'}); 4895 if (!ref($dbh)) { 4896 print STDERR "Failed to open users database : $dbh\n" 4897 } 4898 else { 4899 &disconnect_userdb($config{'userdb'}, $dbh); 4900 } 4901 } 4902} 4903 4904# get_user_details(username, [original-username]) 4905# Returns a hash ref of user details, either from config files or the user DB 4906sub get_user_details 4907{ 4908my ($username, $origusername) = @_; 4909if (exists($users{$username})) { 4910 # In local files 4911 my $two = $twofactor{$origusername} || $twofactor{$username}; 4912 return { 'name' => $username, 4913 'pass' => $users{$username}, 4914 'certs' => $certs{$username}, 4915 'allow' => $allow{$username}, 4916 'deny' => $deny{$username}, 4917 'allowdays' => $allowdays{$username}, 4918 'allowhours' => $allowhours{$username}, 4919 'lastchanges' => $lastchanges{$username}, 4920 'nochange' => $nochange{$username}, 4921 'temppass' => $temppass{$username}, 4922 'preroot' => $config{'preroot_'.$username}, 4923 'twofactor_provider' => $two->{'provider'}, 4924 'twofactor_id' => $two->{'id'}, 4925 'twofactor_apikey' => $two->{'apikey'}, 4926 }; 4927 } 4928if ($config{'userdb'}) { 4929 # Try querying user database 4930 if (exists($get_user_details_cache{$username})) { 4931 # Cached already 4932 return $get_user_details_cache{$username}; 4933 } 4934 print DEBUG "get_user_details: Connecting to user database\n"; 4935 my ($dbh, $proto, $prefix, $args) = &connect_userdb($config{'userdb'}); 4936 my $user; 4937 my %attrs; 4938 if (!ref($dbh)) { 4939 print DEBUG "get_user_details: Failed : $dbh\n"; 4940 print STDERR "Failed to connect to user database : $dbh\n"; 4941 } 4942 elsif ($proto eq "mysql" || $proto eq "postgresql") { 4943 # Fetch user ID and password with SQL 4944 print DEBUG "get_user_details: Looking for $username in SQL\n"; 4945 my $cmd = $dbh->prepare( 4946 "select id,pass from webmin_user where name = ?"); 4947 if (!$cmd || !$cmd->execute($username)) { 4948 print STDERR "Failed to lookup user : ", 4949 $dbh->errstr,"\n"; 4950 return undef; 4951 } 4952 my ($id, $pass) = $cmd->fetchrow(); 4953 $cmd->finish(); 4954 if (!$id) { 4955 &disconnect_userdb($config{'userdb'}, $dbh); 4956 $get_user_details_cache{$username} = undef; 4957 print DEBUG "get_user_details: User not found\n"; 4958 return undef; 4959 } 4960 print DEBUG "get_user_details: id=$id pass=$pass\n"; 4961 4962 # Fetch attributes and add to user object 4963 print DEBUG "get_user_details: finding user attributes\n"; 4964 my $cmd = $dbh->prepare( 4965 "select attr,value from webmin_user_attr where id = ?"); 4966 if (!$cmd || !$cmd->execute($id)) { 4967 print STDERR "Failed to lookup user attrs : ", 4968 $dbh->errstr,"\n"; 4969 return undef; 4970 } 4971 $user = { 'name' => $username, 4972 'id' => $id, 4973 'pass' => $pass, 4974 'proto' => $proto }; 4975 while(my ($attr, $value) = $cmd->fetchrow()) { 4976 $attrs{$attr} = $value; 4977 } 4978 $cmd->finish(); 4979 } 4980 elsif ($proto eq "ldap") { 4981 # Fetch user DN with LDAP 4982 print DEBUG "get_user_details: Looking for $username in LDAP\n"; 4983 my $rv = $dbh->search( 4984 base => $prefix, 4985 filter => '(&(cn='.$username.')(objectClass='. 4986 $args->{'userclass'}.'))', 4987 scope => 'sub'); 4988 if (!$rv || $rv->code) { 4989 print STDERR "Failed to lookup user : ", 4990 ($rv ? $rv->error : "Unknown error"),"\n"; 4991 return undef; 4992 } 4993 my ($u) = $rv->all_entries(); 4994 if (!$u || $u->get_value('cn') ne $username) { 4995 &disconnect_userdb($config{'userdb'}, $dbh); 4996 $get_user_details_cache{$username} = undef; 4997 print DEBUG "get_user_details: User not found\n"; 4998 return undef; 4999 } 5000 5001 # Extract attributes 5002 my $pass = $u->get_value('webminPass'); 5003 $user = { 'name' => $username, 5004 'id' => $u->dn(), 5005 'pass' => $pass, 5006 'proto' => $proto }; 5007 foreach my $la ($u->get_value('webminAttr')) { 5008 my ($attr, $value) = split(/=/, $la, 2); 5009 $attrs{$attr} = $value; 5010 } 5011 } 5012 5013 # Convert DB attributes into user object fields 5014 if ($user) { 5015 print DEBUG "get_user_details: got ",scalar(keys %attrs), 5016 " attributes\n"; 5017 $user->{'certs'} = $attrs{'cert'}; 5018 if ($attrs{'allow'}) { 5019 $user->{'allow'} = $config{'alwaysresolve'} ? 5020 [ split(/\s+/, $attrs{'allow'}) ] : 5021 [ &to_ipaddress(split(/\s+/,$attrs{'allow'})) ]; 5022 } 5023 if ($attrs{'deny'}) { 5024 $user->{'deny'} = $config{'alwaysresolve'} ? 5025 [ split(/\s+/, $attrs{'deny'}) ] : 5026 [ &to_ipaddress(split(/\s+/,$attrs{'deny'})) ]; 5027 } 5028 if ($attrs{'days'}) { 5029 $user->{'allowdays'} = [ split(/,/, $attrs{'days'}) ]; 5030 } 5031 if ($attrs{'hoursfrom'} && $attrs{'hoursto'}) { 5032 my ($hf, $mf) = split(/\./, $attrs{'hoursfrom'}); 5033 my ($ht, $mt) = split(/\./, $attrs{'hoursto'}); 5034 $user->{'allowhours'} = [ $hf*60+$ht, $ht*60+$mt ]; 5035 } 5036 $user->{'lastchanges'} = $attrs{'lastchange'}; 5037 $user->{'nochange'} = $attrs{'nochange'}; 5038 $user->{'temppass'} = $attrs{'temppass'}; 5039 $user->{'preroot'} = $attrs{'theme'}; 5040 $user->{'twofactor_provider'} = $attrs{'twofactor_provider'}; 5041 $user->{'twofactor_id'} = $attrs{'twofactor_id'}; 5042 $user->{'twofactor_apikey'} = $attrs{'twofactor_apikey'}; 5043 } 5044 &disconnect_userdb($config{'userdb'}, $dbh); 5045 $get_user_details_cache{$user->{'name'}} = $user; 5046 return $user; 5047 } 5048return undef; 5049} 5050 5051# find_user_by_cert(cert) 5052# Returns a username looked up by certificate 5053sub find_user_by_cert 5054{ 5055my ($peername) = @_; 5056my $peername2 = $peername; 5057$peername2 =~ s/Email=/emailAddress=/ || $peername2 =~ s/emailAddress=/Email=/; 5058 5059# First check users in local files 5060foreach my $username (keys %certs) { 5061 if ($certs{$username} eq $peername || 5062 $certs{$username} eq $peername2) { 5063 return $username; 5064 } 5065 } 5066 5067# Check user DB 5068if ($config{'userdb'}) { 5069 my ($dbh, $proto) = &connect_userdb($config{'userdb'}); 5070 if (!ref($dbh)) { 5071 return undef; 5072 } 5073 elsif ($proto eq "mysql" || $proto eq "postgresql") { 5074 # Query with SQL 5075 my $cmd = $dbh->prepare("select webmin_user.name from webmin_user,webmin_user_attr where webmin_user.id = webmin_user_attr.id and webmin_user_attr.attr = 'cert' and webmin_user_attr.value = ?"); 5076 return undef if (!$cmd); 5077 foreach my $p ($peername, $peername2) { 5078 my $username; 5079 if ($cmd->execute($p)) { 5080 ($username) = $cmd->fetchrow(); 5081 } 5082 $cmd->finish(); 5083 return $username if ($username); 5084 } 5085 } 5086 elsif ($proto eq "ldap") { 5087 # Lookup in LDAP 5088 my $rv = $dbh->search( 5089 base => $prefix, 5090 filter => '(objectClass='. 5091 $args->{'userclass'}.')', 5092 scope => 'sub', 5093 attrs => [ 'cn', 'webminAttr' ]); 5094 if ($rv && !$rv->code) { 5095 foreach my $u ($rv->all_entries) { 5096 my @attrs = $u->get_value('webminAttr'); 5097 foreach my $la (@attrs) { 5098 my ($attr, $value) = split(/=/, $la, 2); 5099 if ($attr eq "cert" && 5100 ($value eq $peername || 5101 $value eq $peername2)) { 5102 return $u->get_value('cn'); 5103 } 5104 } 5105 } 5106 } 5107 } 5108 } 5109return undef; 5110} 5111 5112# connect_userdb(string) 5113# Returns a handle for talking to a user database - may be a DBI or LDAP handle. 5114# On failure returns an error message string. In an array context, returns the 5115# protocol type too. 5116sub connect_userdb 5117{ 5118my ($str) = @_; 5119my ($proto, $user, $pass, $host, $prefix, $args) = &split_userdb_string($str); 5120if ($proto eq "mysql") { 5121 # Connect to MySQL with DBI 5122 my $drh = eval "use DBI; DBI->install_driver('mysql');"; 5123 $drh || return $text{'sql_emysqldriver'}; 5124 my ($host, $port) = split(/:/, $host); 5125 my $cstr = "database=$prefix;host=$host"; 5126 $cstr .= ";port=$port" if ($port); 5127 print DEBUG "connect_userdb: Connecting to MySQL $cstr as $user\n"; 5128 my $dbh = $drh->connect($cstr, $user, $pass, { }); 5129 $dbh || return "Failed to connect to MySQL : ".$drh->errstr; 5130 print DEBUG "connect_userdb: Connected OK\n"; 5131 return wantarray ? ($dbh, $proto, $prefix, $args) : $dbh; 5132 } 5133elsif ($proto eq "postgresql") { 5134 # Connect to PostgreSQL with DBI 5135 my $drh = eval "use DBI; DBI->install_driver('Pg');"; 5136 $drh || return $text{'sql_epostgresqldriver'}; 5137 my ($host, $port) = split(/:/, $host); 5138 my $cstr = "dbname=$prefix;host=$host"; 5139 $cstr .= ";port=$port" if ($port); 5140 print DEBUG "connect_userdb: Connecting to PostgreSQL $cstr as $user\n"; 5141 my $dbh = $drh->connect($cstr, $user, $pass); 5142 $dbh || return "Failed to connect to PostgreSQL : ".$drh->errstr; 5143 print DEBUG "connect_userdb: Connected OK\n"; 5144 return wantarray ? ($dbh, $proto, $prefix, $args) : $dbh; 5145 } 5146elsif ($proto eq "ldap") { 5147 # Connect with perl LDAP module 5148 eval "use Net::LDAP"; 5149 $@ && return $text{'sql_eldapdriver'}; 5150 my ($host, $port) = split(/:/, $host); 5151 my $scheme = $args->{'scheme'} || 'ldap'; 5152 if (!$port) { 5153 $port = $scheme eq 'ldaps' ? 636 : 389; 5154 } 5155 my $ldap = Net::LDAP->new($host, 5156 port => $port, 5157 'scheme' => $scheme); 5158 $ldap || return "Failed to connect to LDAP : ".$host; 5159 my $mesg; 5160 if ($args->{'tls'}) { 5161 # Switch to TLS mode 5162 eval { $mesg = $ldap->start_tls(); }; 5163 if ($@ || !$mesg || $mesg->code) { 5164 return "Failed to switch to LDAP TLS mode : ". 5165 ($@ ? $@ : $mesg ? $mesg->error : "Unknown error"); 5166 } 5167 } 5168 # Login to the server 5169 if ($pass) { 5170 $mesg = $ldap->bind(dn => $user, password => $pass); 5171 } 5172 else { 5173 $mesg = $ldap->bind(dn => $user, anonymous => 1); 5174 } 5175 if (!$mesg || $mesg->code) { 5176 return "Failed to login to LDAP as ".$user." : ". 5177 ($mesg ? $mesg->error : "Unknown error"); 5178 } 5179 return wantarray ? ($ldap, $proto, $prefix, $args) : $ldap; 5180 } 5181else { 5182 return "Unknown protocol $proto"; 5183 } 5184} 5185 5186# split_userdb_string(string) 5187# Converts a string like mysql://user:pass@host/db into separate parts 5188sub split_userdb_string 5189{ 5190my ($str) = @_; 5191if ($str =~ /^([a-z]+):\/\/([^:]*):([^\@]*)\@([a-z0-9\.\-\_]+)\/([^\?]+)(\?(.*))?$/) { 5192 my ($proto, $user, $pass, $host, $prefix, $argstr) = 5193 ($1, $2, $3, $4, $5, $7); 5194 my %args = map { split(/=/, $_, 2) } split(/\&/, $argstr); 5195 return ($proto, $user, $pass, $host, $prefix, \%args); 5196 } 5197return ( ); 5198} 5199 5200# disconnect_userdb(string, &handle) 5201# Closes a handle opened by connect_userdb 5202sub disconnect_userdb 5203{ 5204my ($str, $h) = @_; 5205if ($str =~ /^(mysql|postgresql):/) { 5206 # DBI disconnnect 5207 $h->disconnect(); 5208 } 5209elsif ($str =~ /^ldap:/) { 5210 # LDAP disconnect 5211 $h->disconnect(); 5212 } 5213} 5214 5215# read_mime_types() 5216# Fills %mime with entries from file in %config and extra settings in %config 5217sub read_mime_types 5218{ 5219undef(%mime); 5220if ($config{"mimetypes"} ne "") { 5221 open(MIME, $config{"mimetypes"}); 5222 while(<MIME>) { 5223 chop; s/#.*$//; 5224 if (/^(\S+)\s+(.*)$/) { 5225 my $type = $1; 5226 my @exts = split(/\s+/, $2); 5227 foreach my $ext (@exts) { 5228 $mime{$ext} = $type; 5229 } 5230 } 5231 } 5232 close(MIME); 5233 } 5234foreach my $k (keys %config) { 5235 if ($k !~ /^addtype_(.*)$/) { next; } 5236 $mime{$1} = $config{$k}; 5237 } 5238} 5239 5240# build_config_mappings() 5241# Build the anonymous access list, IP access list, unauthenticated URLs list, 5242# redirect mapping and allow and deny lists from %config 5243sub build_config_mappings 5244{ 5245# build anonymous access list 5246undef(%anonymous); 5247foreach my $a (split(/\s+/, $config{'anonymous'})) { 5248 if ($a =~ /^([^=]+)=(\S+)$/) { 5249 $anonymous{$1} = $2; 5250 } 5251 } 5252 5253# build IP access list 5254undef(%ipaccess); 5255foreach my $a (split(/\s+/, $config{'ipaccess'})) { 5256 if ($a =~ /^([^=]+)=(\S+)$/) { 5257 $ipaccess{$1} = $2; 5258 } 5259 } 5260 5261# build unauthenticated URLs list 5262@unauth = split(/\s+/, $config{'unauth'}); 5263 5264# build redirect mapping 5265undef(%redirect); 5266foreach my $r (split(/\s+/, $config{'redirect'})) { 5267 if ($r =~ /^([^=]+)=(\S+)$/) { 5268 $redirect{$1} = $2; 5269 } 5270 } 5271 5272# build prefixes to be stripped 5273undef(@strip_prefix); 5274foreach my $r (split(/\s+/, $config{'strip_prefix'})) { 5275 push(@strip_prefix, $r); 5276 } 5277 5278# Init allow and deny lists 5279@deny = split(/\s+/, $config{"deny"}); 5280@deny = &to_ipaddress(@deny) if (!$config{'alwaysresolve'}); 5281@allow = split(/\s+/, $config{"allow"}); 5282@allow = &to_ipaddress(@allow) if (!$config{'alwaysresolve'}); 5283undef(@allowusers); 5284undef(@denyusers); 5285if ($config{'allowusers'}) { 5286 @allowusers = split(/\s+/, $config{'allowusers'}); 5287 } 5288elsif ($config{'denyusers'}) { 5289 @denyusers = split(/\s+/, $config{'denyusers'}); 5290 } 5291 5292# Build list of unixauth mappings 5293undef(%unixauth); 5294foreach my $ua (split(/\s+/, $config{'unixauth'})) { 5295 if ($ua =~ /^(\S+)=(\S+)$/) { 5296 $unixauth{$1} = $2; 5297 } 5298 else { 5299 $unixauth{"*"} = $ua; 5300 } 5301 } 5302 5303# Build list of non-session-auth pages 5304undef(%sessiononly); 5305foreach my $sp (split(/\s+/, $config{'sessiononly'})) { 5306 $sessiononly{$sp} = 1; 5307 } 5308 5309# Build list of logout times 5310undef(@logouttimes); 5311foreach my $a (split(/\s+/, $config{'logouttimes'})) { 5312 if ($a =~ /^([^=]+)=(\S+)$/) { 5313 push(@logouttimes, [ $1, $2 ]); 5314 } 5315 } 5316push(@logouttimes, [ undef, $config{'logouttime'} ]); 5317 5318# Build list of DAV pathss 5319undef(@davpaths); 5320foreach my $d (split(/\s+/, $config{'davpaths'})) { 5321 push(@davpaths, $d); 5322 } 5323@davusers = split(/\s+/, $config{'dav_users'}); 5324 5325# Mobile agent substrings and hostname prefixes 5326@mobile_agents = split(/\t+/, $config{'mobile_agents'}); 5327@mobile_prefixes = split(/\s+/, $config{'mobile_prefixes'}); 5328 5329# Expires time list 5330@expires_paths = ( ); 5331foreach my $pe (split(/\t+/, $config{'expires_paths'})) { 5332 my ($p, $e) = split(/=/, $pe); 5333 if ($p && $e ne '') { 5334 push(@expires_paths, [ $p, $e ]); 5335 } 5336 } 5337 5338# Open debug log 5339close(DEBUG); 5340if ($config{'debug'}) { 5341 open(DEBUG, ">>$config{'debug'}"); 5342 } 5343else { 5344 open(DEBUG, ">/dev/null"); 5345 } 5346 5347# Reset cache of sudo checks 5348undef(%sudocache); 5349} 5350 5351# is_group_member(&uinfo, groupname) 5352# Returns 1 if some user is a primary or secondary member of a group 5353sub is_group_member 5354{ 5355local ($uinfo, $group) = @_; 5356local @ginfo = getgrnam($group); 5357return 0 if (!@ginfo); 5358return 1 if ($ginfo[2] == $uinfo->[3]); # primary member 5359foreach my $m (split(/\s+/, $ginfo[3])) { 5360 return 1 if ($m eq $uinfo->[0]); 5361 } 5362return 0; 5363} 5364 5365# prefix_to_mask(prefix) 5366# Converts a number like 24 to a mask like 255.255.255.0 5367sub prefix_to_mask 5368{ 5369return $_[0] >= 24 ? "255.255.255.".(256-(2 ** (32-$_[0]))) : 5370 $_[0] >= 16 ? "255.255.".(256-(2 ** (24-$_[0]))).".0" : 5371 $_[0] >= 8 ? "255.".(256-(2 ** (16-$_[0]))).".0.0" : 5372 (256-(2 ** (8-$_[0]))).".0.0.0"; 5373} 5374 5375# get_logout_time(user, session-id) 5376# Given a username, returns the idle time before he will be logged out 5377sub get_logout_time 5378{ 5379local ($user, $sid) = @_; 5380if (!defined($logout_time_cache{$user,$sid})) { 5381 local $time; 5382 foreach my $l (@logouttimes) { 5383 if ($l->[0] =~ /^\@(.*)$/) { 5384 # Check group membership 5385 local @uinfo = getpwnam($user); 5386 if (@uinfo && &is_group_member(\@uinfo, $1)) { 5387 $time = $l->[1]; 5388 } 5389 } 5390 elsif ($l->[0] =~ /^\//) { 5391 # Check file contents 5392 open(FILE, $l->[0]); 5393 while(<FILE>) { 5394 s/\r|\n//g; 5395 s/^\s*#.*$//; 5396 if ($user eq $_) { 5397 $time = $l->[1]; 5398 last; 5399 } 5400 } 5401 close(FILE); 5402 } 5403 elsif (!$l->[0]) { 5404 # Always match 5405 $time = $l->[1]; 5406 } 5407 else { 5408 # Check username 5409 if ($l->[0] eq $user) { 5410 $time = $l->[1]; 5411 } 5412 } 5413 last if (defined($time)); 5414 } 5415 $logout_time_cache{$user,$sid} = $time; 5416 } 5417return $logout_time_cache{$user,$sid}; 5418} 5419 5420# password_crypt(password, salt) 5421# If the salt looks like MD5 and we have a library for it, perform MD5 hashing 5422# of a password. Otherwise, do Unix crypt. 5423sub password_crypt 5424{ 5425local ($pass, $salt) = @_; 5426local $rval; 5427if ($salt =~ /^\$1\$/ && $use_md5) { 5428 $rval = &encrypt_md5($pass, $salt); 5429 } 5430elsif ($salt =~ /^\$6\$/ && $use_sha512) { 5431 $rval = &encrypt_sha512($pass, $salt); 5432 } 5433if (!defined($rval) || $salt ne $rval) { 5434 $rval = &unix_crypt($pass, $salt); 5435 } 5436return $rval; 5437} 5438 5439# unix_crypt(password, salt) 5440# Performs standard Unix hashing for a password 5441sub unix_crypt 5442{ 5443local ($pass, $salt) = @_; 5444if ($use_perl_crypt) { 5445 return Crypt::UnixCrypt::crypt($pass, $salt); 5446 } 5447else { 5448 return crypt($pass, $salt); 5449 } 5450} 5451 5452# handle_dav_request(davpath) 5453# Pass a request on to the Net::DAV::Server module 5454sub handle_dav_request 5455{ 5456local ($path) = @_; 5457eval "use Filesys::Virtual::Plain"; 5458eval "use Net::DAV::Server"; 5459eval "use HTTP::Request"; 5460eval "use HTTP::Headers"; 5461 5462if ($Net::DAV::Server::VERSION eq '1.28' && $config{'dav_nolock'}) { 5463 delete $Net::DAV::Server::implemented{lock}; 5464 delete $Net::DAV::Server::implemented{unlock}; 5465 } 5466 5467# Read in request data 5468if (!$posted_data) { 5469 local $clen = $header{"content-length"}; 5470 while(length($posted_data) < $clen) { 5471 $buf = &read_data($clen - length($posted_data)); 5472 if (!length($buf)) { 5473 &http_error(500, "Failed to read POST request"); 5474 } 5475 $posted_data .= $buf; 5476 } 5477 } 5478 5479# For subsequent logging 5480open(MINISERVLOG, ">>$config{'logfile'}"); 5481 5482# Switch to user 5483local $root; 5484local @u = getpwnam($authuser); 5485if ($config{'dav_remoteuser'} && !$< && $validated) { 5486 if (@u) { 5487 if ($u[2] != 0) { 5488 $( = $u[3]; $) = "$u[3] $u[3]"; 5489 ($>, $<) = ($u[2], $u[2]); 5490 } 5491 if ($config{'dav_root'} eq '*') { 5492 $root = $u[7]; 5493 } 5494 } 5495 else { 5496 &http_error(500, "Unix user ".&html_strip($authuser). 5497 " does not exist"); 5498 return 0; 5499 } 5500 } 5501$root ||= $config{'dav_root'}; 5502$root ||= "/"; 5503 5504# Check if this user can use DAV 5505if (@davusers) { 5506 &users_match(\@u, @davusers) || 5507 &http_error(500, "You are not allowed to access DAV"); 5508 } 5509 5510# Create DAV server 5511my $filesys = Filesys::Virtual::Plain->new({root_path => $root}); 5512my $webdav = Net::DAV::Server->new(); 5513$webdav->filesys($filesys); 5514 5515# Make up a request object, and feed to DAV 5516local $ho = HTTP::Headers->new; 5517foreach my $h (keys %header) { 5518 next if (lc($h) eq "connection"); 5519 $ho->header($h => $header{$h}); 5520 } 5521if ($path ne "/") { 5522 $request_uri =~ s/^\Q$path\E//; 5523 $request_uri = "/" if ($request_uri eq ""); 5524 } 5525my $request = HTTP::Request->new($method, $request_uri, $ho, 5526 $posted_data); 5527if ($config{'dav_debug'}) { 5528 print STDERR "DAV request :\n"; 5529 print STDERR "---------------------------------------------\n"; 5530 print STDERR $request->as_string(); 5531 print STDERR "---------------------------------------------\n"; 5532 } 5533my $response = $webdav->run($request); 5534 5535# Send back the reply 5536&write_data("HTTP/1.1 ",$response->code()," ",$response->message(),"\r\n"); 5537local $content = $response->content(); 5538if ($path ne "/") { 5539 $content =~ s|href>/(.+)<|href>$path/$1<|g; 5540 $content =~ s|href>/<|href>$path<|g; 5541 } 5542foreach my $h ($response->header_field_names) { 5543 next if (lc($h) eq "connection" || lc($h) eq "content-length"); 5544 &write_data("$h: ",$response->header($h),"\r\n"); 5545 } 5546&write_data("Content-length: ",length($content),"\r\n"); 5547local $rv = &write_keep_alive(0); 5548&write_data("\r\n"); 5549&write_data($content); 5550 5551if ($config{'dav_debug'}) { 5552 print STDERR "DAV reply :\n"; 5553 print STDERR "---------------------------------------------\n"; 5554 print STDERR "HTTP/1.1 ",$response->code()," ",$response->message(),"\r\n"; 5555 foreach my $h ($response->header_field_names) { 5556 next if (lc($h) eq "connection" || lc($h) eq "content-length"); 5557 print STDERR "$h: ",$response->header($h),"\r\n"; 5558 } 5559 print STDERR "Content-length: ",length($content),"\r\n"; 5560 print STDERR "\r\n"; 5561 print STDERR $content; 5562 print STDERR "---------------------------------------------\n"; 5563 } 5564 5565# Log it 5566&log_request($loghost, $authuser, $reqline, $response->code(), 5567 length($response->content())); 5568} 5569 5570# get_system_hostname() 5571# Returns the hostname of this system, for reporting to listeners 5572sub get_system_hostname 5573{ 5574# On Windows, try computername environment variable 5575return $ENV{'computername'} if ($ENV{'computername'}); 5576return $ENV{'COMPUTERNAME'} if ($ENV{'COMPUTERNAME'}); 5577 5578# If a specific command is set, use it first 5579if ($config{'hostname_command'}) { 5580 local $out = `($config{'hostname_command'}) 2>&1`; 5581 if (!$?) { 5582 $out =~ s/\r|\n//g; 5583 return $out; 5584 } 5585 } 5586 5587# First try the hostname command 5588local $out = `hostname 2>&1`; 5589if (!$? && $out =~ /\S/) { 5590 $out =~ s/\r|\n//g; 5591 return $out; 5592 } 5593 5594# Try the Sys::Hostname module 5595eval "use Sys::Hostname"; 5596if (!$@) { 5597 local $rv = eval "hostname()"; 5598 if (!$@ && $rv) { 5599 return $rv; 5600 } 5601 } 5602 5603# Must use net name on Windows 5604local $out = `net name 2>&1`; 5605if ($out =~ /\-+\r?\n(\S+)/) { 5606 return $1; 5607 } 5608 5609return undef; 5610} 5611 5612# indexof(string, array) 5613# Returns the index of some value in an array, or -1 5614sub indexof { 5615 local($i); 5616 for($i=1; $i <= $#_; $i++) { 5617 if ($_[$i] eq $_[0]) { return $i - 1; } 5618 } 5619 return -1; 5620} 5621 5622 5623# has_command(command) 5624# Returns the full path if some command is in the path, undef if not 5625sub has_command 5626{ 5627local($d); 5628if (!$_[0]) { return undef; } 5629if (exists($has_command_cache{$_[0]})) { 5630 return $has_command_cache{$_[0]}; 5631 } 5632local $rv = undef; 5633if ($_[0] =~ /^\//) { 5634 $rv = -x $_[0] ? $_[0] : undef; 5635 } 5636else { 5637 local $sp = $on_windows ? ';' : ':'; 5638 foreach $d (split($sp, $ENV{PATH})) { 5639 if (-x "$d/$_[0]") { 5640 $rv = "$d/$_[0]"; 5641 last; 5642 } 5643 if ($on_windows) { 5644 foreach my $sfx (".exe", ".com", ".bat") { 5645 if (-r "$d/$_[0]".$sfx) { 5646 $rv = "$d/$_[0]".$sfx; 5647 last; 5648 } 5649 } 5650 } 5651 } 5652 } 5653$has_command_cache{$_[0]} = $rv; 5654return $rv; 5655} 5656 5657# check_sudo_permissions(user, pass) 5658# Returns 1 if some user can run any command via sudo 5659sub check_sudo_permissions 5660{ 5661local ($user, $pass) = @_; 5662 5663# First try the pipes 5664if ($PASSINw) { 5665 print DEBUG "check_sudo_permissions: querying cache for $user\n"; 5666 print $PASSINw "readsudo $user\n"; 5667 local $can = <$PASSOUTr>; 5668 chop($can); 5669 print DEBUG "check_sudo_permissions: cache said $can\n"; 5670 if ($can =~ /^\d+$/ && $can != 2) { 5671 return int($can); 5672 } 5673 } 5674 5675local $ptyfh = new IO::Pty; 5676print DEBUG "check_sudo_permissions: ptyfh=$ptyfh\n"; 5677if (!$ptyfh) { 5678 print STDERR "Failed to create new PTY with IO::Pty\n"; 5679 return 0; 5680 } 5681local @uinfo = getpwnam($user); 5682if (!@uinfo) { 5683 print STDERR "Unix user $user does not exist for sudo\n"; 5684 return 0; 5685 } 5686 5687# Execute sudo in a sub-process, via a pty 5688local $ttyfh = $ptyfh->slave(); 5689print DEBUG "check_sudo_permissions: ttyfh=$ttyfh\n"; 5690local $tty = $ptyfh->ttyname(); 5691print DEBUG "check_sudo_permissions: tty=$tty\n"; 5692chown($uinfo[2], $uinfo[3], $tty); 5693pipe(SUDOr, SUDOw); 5694print DEBUG "check_sudo_permissions: about to fork..\n"; 5695local $pid = fork(); 5696print DEBUG "check_sudo_permissions: fork=$pid pid=$$\n"; 5697if ($pid < 0) { 5698 print STDERR "fork for sudo failed : $!\n"; 5699 return 0; 5700 } 5701if (!$pid) { 5702 setsid(); 5703 ($(, $)) = ( $uinfo[3], 5704 "$uinfo[3] ".join(" ", $uinfo[3], 5705 &other_groups($uinfo[0])) ); 5706 ($>, $<) = ($uinfo[2], $uinfo[2]); 5707 $ENV{'USER'} = $ENV{'LOGNAME'} = $user; 5708 $ENV{'HOME'} = $uinfo[7]; 5709 5710 $ptyfh->make_slave_controlling_terminal(); 5711 close(STDIN); close(STDOUT); close(STDERR); 5712 untie(*STDIN); untie(*STDOUT); untie(*STDERR); 5713 close($PASSINw); close($PASSOUTr); 5714 close(SUDOw); 5715 close(SOCK); 5716 close(MAIN); 5717 open(STDIN, "<&SUDOr"); 5718 open(STDOUT, ">$tty"); 5719 open(STDERR, ">&STDOUT"); 5720 close($ptyfh); 5721 exec("sudo -l -S"); 5722 print "Exec failed : $!\n"; 5723 exit 1; 5724 } 5725print DEBUG "check_sudo_permissions: pid=$pid\n"; 5726close(SUDOr); 5727$ptyfh->close_slave(); 5728 5729# Send password, and get back response 5730local $oldfh = select(SUDOw); 5731$| = 1; 5732select($oldfh); 5733print DEBUG "check_sudo_permissions: about to send pass\n"; 5734local $SIG{'PIPE'} = 'ignore'; # Sometimes sudo doesn't ask for a password 5735print SUDOw $pass,"\n"; 5736print DEBUG "check_sudo_permissions: sent pass=$pass\n"; 5737close(SUDOw); 5738local $out; 5739while(<$ptyfh>) { 5740 print DEBUG "check_sudo_permissions: got $_"; 5741 $out .= $_; 5742 } 5743close($ptyfh); 5744kill('KILL', $pid); 5745waitpid($pid, 0); 5746local ($ok) = ($out =~ /\(ALL\)\s+ALL|\(ALL\)\s+NOPASSWD:\s+ALL|\(ALL\s*:\s*ALL\)\s+ALL|\(ALL\s*:\s*ALL\)\s+NOPASSWD:\s+ALL/ ? 1 : 0); 5747 5748# Update cache 5749if ($PASSINw) { 5750 print $PASSINw "writesudo $user $ok\n"; 5751 } 5752 5753return $ok; 5754} 5755 5756sub other_groups 5757{ 5758my ($user) = @_; 5759my @rv; 5760setgrent(); 5761while(my @g = getgrent()) { 5762 my @m = split(/\s+/, $g[3]); 5763 push(@rv, $g[2]) if (&indexof($user, @m) >= 0); 5764 } 5765endgrent(); 5766return @rv; 5767} 5768 5769# is_mobile_useragent(agent) 5770# Returns 1 if some user agent looks like a cellphone or other mobile device, 5771# such as a treo. 5772sub is_mobile_useragent 5773{ 5774local ($agent) = @_; 5775local @prefixes = ( 5776 "UP.Link", # Openwave 5777 "Nokia", # All Nokias start with Nokia 5778 "MOT-", # All Motorola phones start with MOT- 5779 "SAMSUNG", # Samsung browsers 5780 "Samsung", # Samsung browsers 5781 "SEC-", # Samsung browsers 5782 "AU-MIC", # Samsung browsers 5783 "AUDIOVOX", # Audiovox 5784 "BlackBerry", # BlackBerry 5785 "hiptop", # Danger hiptop Sidekick 5786 "SonyEricsson", # Sony Ericsson 5787 "Ericsson", # Old Ericsson browsers , mostly WAP 5788 "Mitsu/1.1.A", # Mitsubishi phones 5789 "Panasonic WAP", # Panasonic old WAP phones 5790 "DoCoMo", # DoCoMo phones 5791 "Lynx", # Lynx text-mode linux browser 5792 "Links", # Another text-mode linux browser 5793 "Dalvik", # Android browser 5794 ); 5795local @substrings = ( 5796 "UP.Browser", # Openwave 5797 "MobilePhone", # NetFront 5798 "AU-MIC-A700", # Samsung A700 Obigo browsers 5799 "Danger hiptop", # Danger Sidekick hiptop 5800 "Windows CE", # Windows CE Pocket PC 5801 "IEMobile", # Windows mobile browser 5802 "Blazer", # Palm Treo Blazer 5803 "BlackBerry", # BlackBerries can emulate other browsers, but 5804 # they still keep this string in the UserAgent 5805 "SymbianOS", # New Series60 browser has safari in it and 5806 # SymbianOS is the only distinguishing string 5807 "iPhone", # Apple iPhone KHTML browser 5808 "iPod", # iPod touch browser 5809 "MobileSafari", # HTTP client in iPhone 5810 "Mobile Safari", # Samsung Galaxy S6 browser 5811 "Opera Mini", # Opera Mini 5812 "HTC_P3700", # HTC mobile device 5813 "Pre/", # Palm Pre 5814 "webOS/", # Palm WebOS 5815 "Nintendo DS", # DSi / DSi-XL 5816 ); 5817local @regexps = ( 5818 "Android.*Mobile", # Android phone 5819 ); 5820foreach my $p (@prefixes) { 5821 return 1 if ($agent =~ /^\Q$p\E/); 5822 } 5823foreach my $s (@substrings, @mobile_agents) { 5824 return 1 if ($agent =~ /\Q$s\E/); 5825 } 5826foreach my $s (@regexps) { 5827 return 1 if ($agent =~ /$s/); 5828 } 5829return 0; 5830} 5831 5832# write_blocked_file() 5833# Writes out a text file of blocked hosts and users 5834sub write_blocked_file 5835{ 5836open(BLOCKED, ">$config{'blockedfile'}"); 5837foreach my $d (grep { $hostfail{$_} } @deny) { 5838 print BLOCKED "host $d $hostfail{$d} $blockhosttime{$d}\n"; 5839 } 5840foreach my $d (grep { $userfail{$_} } @denyusers) { 5841 print BLOCKED "user $d $userfail{$d} $blockusertime{$d}\n"; 5842 } 5843close(BLOCKED); 5844chmod(0700, $config{'blockedfile'}); 5845} 5846 5847sub write_pid_file 5848{ 5849open(PIDFILE, ">$config{'pidfile'}"); 5850printf PIDFILE "%d\n", getpid(); 5851close(PIDFILE); 5852$miniserv_main_pid = getpid(); 5853} 5854 5855# lock_user_password(user) 5856# Updates a user's password file entry to lock it, both in memory and on disk. 5857# Returns 1 if done, -1 if no such user, 0 if already locked 5858sub lock_user_password 5859{ 5860local ($user) = @_; 5861local $uinfo = &get_user_details($user); 5862if (!$uinfo) { 5863 # No such user! 5864 return -1; 5865 } 5866if ($uinfo->{'pass'} =~ /^\!/) { 5867 # Already locked 5868 return 0; 5869 } 5870if (!$uinfo->{'proto'}) { 5871 # Write to users file 5872 $users{$user} = "!".$users{$user}; 5873 open(USERS, $config{'userfile'}); 5874 local @ufile = <USERS>; 5875 close(USERS); 5876 foreach my $u (@ufile) { 5877 local @uinfo = split(/:/, $u); 5878 if ($uinfo[0] eq $user) { 5879 $uinfo[1] = $users{$user}; 5880 } 5881 $u = join(":", @uinfo); 5882 } 5883 open(USERS, ">$config{'userfile'}"); 5884 print USERS @ufile; 5885 close(USERS); 5886 return 0; 5887 } 5888 5889if ($config{'userdb'}) { 5890 # Update user DB 5891 my ($dbh, $proto, $prefix, $args) = &connect_userdb($config{'userdb'}); 5892 if (!$dbh) { 5893 return -1; 5894 } 5895 elsif ($proto eq "mysql" || $proto eq "postgresql") { 5896 # Update user attribute 5897 my $cmd = $dbh->prepare( 5898 "update webmin_user set pass = ? where id = ?"); 5899 if (!$cmd || !$cmd->execute("!".$uinfo->{'pass'}, 5900 $uinfo->{'id'})) { 5901 # Update failed 5902 print STDERR "Failed to lock password : ", 5903 $dbh->errstr,"\n"; 5904 return -1; 5905 } 5906 $cmd->finish() if ($cmd); 5907 } 5908 elsif ($proto eq "ldap") { 5909 # Update LDAP object 5910 my $rv = $dbh->modify($uinfo->{'id'}, 5911 replace => { 'webminPass' => '!'.$uinfo->{'pass'} }); 5912 if (!$rv || $rv->code) { 5913 print STDERR "Failed to lock password : ", 5914 ($rv ? $rv->error : "Unknown error"),"\n"; 5915 return -1; 5916 } 5917 } 5918 &disconnect_userdb($config{'userdb'}, $dbh); 5919 return 0; 5920 } 5921 5922return -1; # This should never be reached 5923} 5924 5925# hash_session_id(sid) 5926# Returns an MD5 or Unix-crypted session ID 5927sub hash_session_id 5928{ 5929local ($sid) = @_; 5930if (!$hash_session_id_cache{$sid}) { 5931 if ($use_md5) { 5932 # Take MD5 hash 5933 $hash_session_id_cache{$sid} = &encrypt_md5($sid); 5934 } 5935 else { 5936 # Unix crypt 5937 $hash_session_id_cache{$sid} = &unix_crypt($sid, "XX"); 5938 } 5939 } 5940return $hash_session_id_cache{$sid}; 5941} 5942 5943# encrypt_md5(string, [salt]) 5944# Returns a string encrypted in MD5 format 5945sub encrypt_md5 5946{ 5947local ($passwd, $salt) = @_; 5948local $magic = '$1$'; 5949if ($salt =~ /^\$1\$([^\$]+)/) { 5950 # Extract actual salt from already encrypted password 5951 $salt = $1; 5952 } 5953 5954# Add the password 5955local $ctx = eval "new $use_md5"; 5956$ctx->add($passwd); 5957if ($salt) { 5958 $ctx->add($magic); 5959 $ctx->add($salt); 5960 } 5961 5962# Add some more stuff from the hash of the password and salt 5963local $ctx1 = eval "new $use_md5"; 5964$ctx1->add($passwd); 5965if ($salt) { 5966 $ctx1->add($salt); 5967 } 5968$ctx1->add($passwd); 5969local $final = $ctx1->digest(); 5970for($pl=length($passwd); $pl>0; $pl-=16) { 5971 $ctx->add($pl > 16 ? $final : substr($final, 0, $pl)); 5972 } 5973 5974# This piece of code seems rather pointless, but it's in the C code that 5975# does MD5 in PAM so it has to go in! 5976local $j = 0; 5977local ($i, $l); 5978for($i=length($passwd); $i; $i >>= 1) { 5979 if ($i & 1) { 5980 $ctx->add("\0"); 5981 } 5982 else { 5983 $ctx->add(substr($passwd, $j, 1)); 5984 } 5985 } 5986$final = $ctx->digest(); 5987 5988if ($salt) { 5989 # This loop exists only to waste time 5990 for($i=0; $i<1000; $i++) { 5991 $ctx1 = eval "new $use_md5"; 5992 $ctx1->add($i & 1 ? $passwd : $final); 5993 $ctx1->add($salt) if ($i % 3); 5994 $ctx1->add($passwd) if ($i % 7); 5995 $ctx1->add($i & 1 ? $final : $passwd); 5996 $final = $ctx1->digest(); 5997 } 5998 } 5999 6000# Convert the 16-byte final string into a readable form 6001local $rv; 6002local @final = map { ord($_) } split(//, $final); 6003$l = ($final[ 0]<<16) + ($final[ 6]<<8) + $final[12]; 6004$rv .= &to64($l, 4); 6005$l = ($final[ 1]<<16) + ($final[ 7]<<8) + $final[13]; 6006$rv .= &to64($l, 4); 6007$l = ($final[ 2]<<16) + ($final[ 8]<<8) + $final[14]; 6008$rv .= &to64($l, 4); 6009$l = ($final[ 3]<<16) + ($final[ 9]<<8) + $final[15]; 6010$rv .= &to64($l, 4); 6011$l = ($final[ 4]<<16) + ($final[10]<<8) + $final[ 5]; 6012$rv .= &to64($l, 4); 6013$l = $final[11]; 6014$rv .= &to64($l, 2); 6015 6016# Add salt if needed 6017if ($salt) { 6018 return $magic.$salt.'$'.$rv; 6019 } 6020else { 6021 return $rv; 6022 } 6023} 6024 6025# encrypt_sha512(password, [salt]) 6026# Hashes a password, possibly with the given salt, with SHA512 6027sub encrypt_sha512 6028{ 6029my ($passwd, $salt) = @_; 6030if ($salt =~ /^\$6\$([^\$]+)/) { 6031 # Extract actual salt from already encrypted password 6032 $salt = $1; 6033 } 6034$salt ||= '$6$'.substr(time(), -8).'$'; 6035return crypt($passwd, $salt); 6036} 6037 6038sub to64 6039{ 6040local ($v, $n) = @_; 6041local $r; 6042while(--$n >= 0) { 6043 $r .= $itoa64[$v & 0x3f]; 6044 $v >>= 6; 6045 } 6046return $r; 6047} 6048 6049# read_file(file, &assoc, [&order], [lowercase]) 6050# Fill an associative array with name=value pairs from a file 6051sub read_file 6052{ 6053open(ARFILE, $_[0]) || return 0; 6054while(<ARFILE>) { 6055 s/\r|\n//g; 6056 if (!/^#/ && /^([^=]*)=(.*)$/) { 6057 $_[1]->{$_[3] ? lc($1) : $1} = $2; 6058 push(@{$_[2]}, $1) if ($_[2]); 6059 } 6060 } 6061close(ARFILE); 6062return 1; 6063} 6064 6065# write_file(file, array) 6066# Write out the contents of an associative array as name=value lines 6067sub write_file 6068{ 6069local(%old, @order); 6070&read_file($_[0], \%old, \@order); 6071open(ARFILE, ">$_[0]"); 6072foreach $k (@order) { 6073 print ARFILE $k,"=",$_[1]->{$k},"\n" if (exists($_[1]->{$k})); 6074 } 6075foreach $k (keys %{$_[1]}) { 6076 print ARFILE $k,"=",$_[1]->{$k},"\n" if (!exists($old{$k})); 6077 } 6078close(ARFILE); 6079} 6080 6081# execute_ready_webmin_crons(run-count) 6082# Find and run any cron jobs that are due, based on their last run time and 6083# execution interval 6084sub execute_ready_webmin_crons 6085{ 6086my ($runs) = @_; 6087my $now = time(); 6088my $changed = 0; 6089foreach my $cron (@webmincrons) { 6090 my $run = 0; 6091 if ($runs == 0 && $cron->{'boot'}) { 6092 # If cron job wants to be run at startup, run it now 6093 $run = 1; 6094 } 6095 elsif ($cron->{'disabled'}) { 6096 # Explicitly disabled 6097 $run = 0; 6098 } 6099 elsif (!$webmincron_last{$cron->{'id'}}) { 6100 # If not ever run before, don't run right away 6101 $webmincron_last{$cron->{'id'}} = $now; 6102 $changed = 1; 6103 } 6104 elsif ($cron->{'interval'} && 6105 $now - $webmincron_last{$cron->{'id'}} > $cron->{'interval'}) { 6106 # Older than interval .. time to run 6107 $run = 1; 6108 } 6109 elsif ($cron->{'mins'} ne '') { 6110 # Check if current time matches spec, and we haven't run in the 6111 # last minute 6112 my @tm = localtime($now); 6113 if (&matches_cron($cron->{'mins'}, $tm[1], 0) && 6114 &matches_cron($cron->{'hours'}, $tm[2], 0) && 6115 &matches_cron($cron->{'days'}, $tm[3], 1) && 6116 &matches_cron($cron->{'months'}, $tm[4]+1, 1) && 6117 &matches_cron($cron->{'weekdays'}, $tm[6], 0) && 6118 $now - $webmincron_last{$cron->{'id'}} > 60) { 6119 $run = 1; 6120 } 6121 } 6122 6123 if ($run) { 6124 print DEBUG "Running cron id=$cron->{'id'} ". 6125 "module=$cron->{'module'} func=$cron->{'func'} ". 6126 "arg0=$cron->{'arg0'}\n"; 6127 $webmincron_last{$cron->{'id'}} = $now; 6128 $changed = 1; 6129 my $pid = &execute_webmin_command($config{'webmincron_wrapper'}, 6130 [ $cron ]); 6131 push(@childpids, $pid); 6132 } 6133 } 6134if ($changed) { 6135 # Write out file containing last run times 6136 &write_file($config{'webmincron_last'}, \%webmincron_last); 6137 } 6138} 6139 6140# matches_cron(cron-spec, time, first-value) 6141# Checks if some minute or hour matches some cron spec, which can be * or a list 6142# of numbers. 6143sub matches_cron 6144{ 6145my ($spec, $tm, $first) = @_; 6146if ($spec eq '*') { 6147 return 1; 6148 } 6149else { 6150 foreach my $s (split(/,/, $spec)) { 6151 if ($s == $tm || 6152 $s =~ /^(\d+)\-(\d+)$/ && 6153 $tm >= $1 && $tm <= $2 || 6154 $s =~ /^\*\/(\d+)$/ && 6155 $tm % $1 == $first || 6156 $s =~ /^(\d+)\-(\d+)\/(\d+)$/ && 6157 $tm >= $1 && $tm <= $2 && $tm % $3 == $first) { 6158 return 1; 6159 } 6160 } 6161 return 0; 6162 } 6163} 6164 6165# read_webmin_crons() 6166# Read all scheduled webmin cron functions and store them in the @webmincrons 6167# global list 6168sub read_webmin_crons 6169{ 6170@webmincrons = ( ); 6171opendir(CRONS, $config{'webmincron_dir'}); 6172print DEBUG "Reading crons from $config{'webmincron_dir'}\n"; 6173foreach my $f (readdir(CRONS)) { 6174 if ($f =~ /^(\d+)\.cron$/) { 6175 my %cron; 6176 &read_file("$config{'webmincron_dir'}/$f", \%cron); 6177 $cron{'id'} = $1; 6178 my $broken = 0; 6179 foreach my $n ('module', 'func') { 6180 if (!$cron{$n}) { 6181 print STDERR "Cron $1 missing $n\n"; 6182 $broken = 1; 6183 } 6184 } 6185 if (!$cron{'interval'} && $cron{'mins'} eq '' && 6186 $cron{'special'} eq '' && !$cron{'boot'}) { 6187 print STDERR "Cron $1 missing any time spec\n"; 6188 $broken = 1; 6189 } 6190 if ($cron{'special'} eq 'hourly') { 6191 # Run every hour on the hour 6192 $cron{'mins'} = 0; 6193 $cron{'hours'} = '*'; 6194 $cron{'days'} = '*'; 6195 $cron{'months'} = '*'; 6196 $cron{'weekdays'} = '*'; 6197 } 6198 elsif ($cron{'special'} eq 'daily') { 6199 # Run every day at midnight 6200 $cron{'mins'} = 0; 6201 $cron{'hours'} = '0'; 6202 $cron{'days'} = '*'; 6203 $cron{'months'} = '*'; 6204 $cron{'weekdays'} = '*'; 6205 } 6206 elsif ($cron{'special'} eq 'monthly') { 6207 # Run every month on the 1st 6208 $cron{'mins'} = 0; 6209 $cron{'hours'} = '0'; 6210 $cron{'days'} = '1'; 6211 $cron{'months'} = '*'; 6212 $cron{'weekdays'} = '*'; 6213 } 6214 elsif ($cron{'special'} eq 'weekly') { 6215 # Run every month on the 1st 6216 $cron{'mins'} = 0; 6217 $cron{'hours'} = '0'; 6218 $cron{'days'} = '*'; 6219 $cron{'months'} = '*'; 6220 $cron{'weekdays'} = '0'; 6221 } 6222 elsif ($cron{'special'} eq 'yearly' || 6223 $cron{'special'} eq 'annually') { 6224 # Run every year on 1st january 6225 $cron{'mins'} = 0; 6226 $cron{'hours'} = '0'; 6227 $cron{'days'} = '1'; 6228 $cron{'months'} = '1'; 6229 $cron{'weekdays'} = '*'; 6230 } 6231 elsif ($cron{'special'}) { 6232 print STDERR "Cron $1 invalid special time $cron{'special'}\n"; 6233 $broken = 1; 6234 } 6235 if ($cron{'special'}) { 6236 delete($cron{'special'}); 6237 } 6238 if (!$broken) { 6239 print DEBUG "Adding cron id=$cron{'id'} module=$cron{'module'} func=$cron{'func'} arg0=$cron{'arg0'}\n"; 6240 push(@webmincrons, \%cron); 6241 } 6242 } 6243 } 6244closedir(CRONS); 6245} 6246 6247# precache_files() 6248# Read into the Webmin cache all files marked for pre-caching 6249sub precache_files 6250{ 6251undef(%main::read_file_cache); 6252foreach my $g (split(/\s+/, $config{'precache'})) { 6253 next if ($g eq "none"); 6254 foreach my $f (glob("$config{'root'}/$g")) { 6255 my @st = stat($f); 6256 next if (!@st); 6257 $main::read_file_cache{$f} = { }; 6258 &read_file($f, $main::read_file_cache{$f}); 6259 $main::read_file_cache_time{$f} = $st[9]; 6260 } 6261 } 6262} 6263 6264# Check if some address is valid IPv4, returns 1 if so. 6265sub check_ipaddress 6266{ 6267return $_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ && 6268 $1 >= 0 && $1 <= 255 && 6269 $2 >= 0 && $2 <= 255 && 6270 $3 >= 0 && $3 <= 255 && 6271 $4 >= 0 && $4 <= 255; 6272} 6273 6274# Check if some IPv6 address is properly formatted, and returns 1 if so. 6275sub check_ip6address 6276{ 6277 my @blocks = split(/:/, $_[0]); 6278 return 0 if (@blocks == 0 || @blocks > 8); 6279 my $ib = $#blocks; 6280 my $where = index($blocks[$ib],"/"); 6281 my $m = 0; 6282 if ($where != -1) { 6283 my $b = substr($blocks[$ib],0,$where); 6284 $m = substr($blocks[$ib],$where+1,length($blocks[$ib])-($where+1)); 6285 $blocks[$ib]=$b; 6286 } 6287 return 0 if ($m <0 || $m >128); 6288 my $b; 6289 my $empty = 0; 6290 foreach $b (@blocks) { 6291 return 0 if ($b ne "" && $b !~ /^[0-9a-f]{1,4}$/i); 6292 $empty++ if ($b eq ""); 6293 } 6294 return 0 if ($empty > 1 && !($_[0] =~ /^::/ && $empty == 2)); 6295 return 1; 6296} 6297 6298# network_to_address(binary) 6299# Given a network address in binary IPv4 or v4 format, return the string form 6300sub network_to_address 6301{ 6302local ($addr) = @_; 6303if (length($addr) == 4 || !$use_ipv6) { 6304 return inet_ntoa($addr); 6305 } 6306else { 6307 return inet_ntop(AF_INET6(), $addr); 6308 } 6309} 6310 6311# redirect_stderr_to_log() 6312# Re-direct STDERR to error log file 6313sub redirect_stderr_to_log 6314{ 6315if ($config{'errorlog'} ne '-') { 6316 open(STDERR, ">>$config{'errorlog'}") || 6317 die "failed to open $config{'errorlog'} : $!"; 6318 if ($config{'logperms'}) { 6319 chmod(oct($config{'logperms'}), $config{'errorlog'}); 6320 } 6321 } 6322select(STDERR); $| = 1; select(STDOUT); 6323} 6324 6325# should_gzip_file(filename) 6326# Returns 1 if some path should be gzipped 6327sub should_gzip_file 6328{ 6329my ($path) = @_; 6330return $path !~ /\.(gif|png|jpg|jpeg|tif|tiff)$/i; 6331} 6332 6333# get_expires_time(path) 6334# Given a URL path, return the client-side expiry time in seconds 6335sub get_expires_time 6336{ 6337my ($path) = @_; 6338foreach my $pe (@expires_paths) { 6339 if ($path =~ /$pe->[0]/i) { 6340 return $pe->[1]; 6341 } 6342 } 6343return $config{'expires'}; 6344} 6345 6346sub html_escape 6347{ 6348my ($tmp) = @_; 6349$tmp =~ s/&/&/g; 6350$tmp =~ s/</</g; 6351$tmp =~ s/>/>/g; 6352$tmp =~ s/\"/"/g; 6353$tmp =~ s/\'/'/g; 6354$tmp =~ s/=/=/g; 6355return $tmp; 6356} 6357 6358sub html_strip 6359{ 6360my ($tmp) = @_; 6361$tmp =~ s/<[^>]*>//g; 6362return $tmp; 6363} 6364 6365# validate_twofactor(username, token, orig-username) 6366# Checks if a user's two-factor token is valid or not. Returns undef on success 6367# or the error message on failure. 6368sub validate_twofactor 6369{ 6370my ($user, $token, $origuser) = @_; 6371local $uinfo = &get_user_details($user, $origuser); 6372$token =~ s/^\s+//; 6373$token =~ s/\s+$//; 6374$token || return "No two-factor token entered"; 6375$uinfo->{'twofactor_provider'} || return undef; 6376pipe(TOKENr, TOKENw); 6377my $pid = &execute_webmin_command($config{'twofactor_wrapper'}, 6378 [ $user, $uinfo->{'twofactor_provider'}, $uinfo->{'twofactor_id'}, 6379 $token, $uinfo->{'twofactor_apikey'} ], 6380 TOKENw); 6381close(TOKENw); 6382waitpid($pid, 0); 6383my $ex = $?; 6384my $out = <TOKENr>; 6385close(TOKENr); 6386if ($ex) { 6387 return $out || "Unknown two-factor authentication failure"; 6388 } 6389return undef; 6390} 6391 6392# execute_webmin_command(command, &argv, [stdout-fd]) 6393# Run some Webmin script in a sub-process, like webmincron.pl 6394# Returns the PID of the new process. 6395sub execute_webmin_command 6396{ 6397my ($cmd, $argv, $fd) = @_; 6398my $pid = fork(); 6399if (!$pid) { 6400 # Run via a wrapper command, which we run like a CGI 6401 dbmclose(%sessiondb); 6402 if ($fd) { 6403 open(STDOUT, ">&$fd"); 6404 } 6405 else { 6406 open(STDOUT, ">&STDERR"); 6407 } 6408 &close_all_sockets(); 6409 &close_all_pipes(); 6410 close(LISTEN); 6411 6412 # Setup CGI-like environment 6413 $envtz = $ENV{"TZ"}; 6414 $envuser = $ENV{"USER"}; 6415 $envpath = $ENV{"PATH"}; 6416 $envlang = $ENV{"LANG"}; 6417 $envroot = $ENV{"SystemRoot"}; 6418 $envperllib = $ENV{'PERLLIB'}; 6419 foreach my $k (keys %ENV) { 6420 delete($ENV{$k}); 6421 } 6422 $ENV{"PATH"} = $envpath if ($envpath); 6423 $ENV{"TZ"} = $envtz if ($envtz); 6424 $ENV{"USER"} = $envuser if ($envuser); 6425 $ENV{"OLD_LANG"} = $envlang if ($envlang); 6426 $ENV{"SystemRoot"} = $envroot if ($envroot); 6427 $ENV{'PERLLIB'} = $envperllib if ($envperllib); 6428 $ENV{"HOME"} = $user_homedir; 6429 $ENV{"SERVER_SOFTWARE"} = $config{"server"}; 6430 $ENV{"SERVER_ADMIN"} = $config{"email"}; 6431 $root0 = $roots[0]; 6432 $ENV{"SERVER_ROOT"} = $root0; 6433 $ENV{"SERVER_REALROOT"} = $root0; 6434 $ENV{"SERVER_PORT"} = $config{'port'}; 6435 $ENV{"WEBMIN_CRON"} = 1; 6436 $ENV{"DOCUMENT_ROOT"} = $root0; 6437 $ENV{"THEME_ROOT"} = $root0."/".$config{"preroot"}; 6438 $ENV{"THEME_DIRS"} = $config{"preroot"} || ""; 6439 $ENV{"DOCUMENT_REALROOT"} = $root0; 6440 $ENV{"MINISERV_CONFIG"} = $config_file; 6441 $ENV{"HTTPS"} = "ON" if ($use_ssl); 6442 $ENV{"MINISERV_PID"} = $miniserv_main_pid; 6443 $ENV{"SCRIPT_FILENAME"} = $cmd; 6444 if ($ENV{"SCRIPT_FILENAME"} =~ /^\Q$root0\E(\/.*)$/) { 6445 $ENV{"SCRIPT_NAME"} = $1; 6446 } 6447 $cmd =~ /^(.*)\//; 6448 $ENV{"PWD"} = $1; 6449 foreach $k (keys %config) { 6450 if ($k =~ /^env_(\S+)$/) { 6451 $ENV{$1} = $config{$k}; 6452 } 6453 } 6454 chdir($ENV{"PWD"}); 6455 $SIG{'CHLD'} = 'DEFAULT'; 6456 eval { 6457 # Have SOCK closed if the perl exec's something 6458 use Fcntl; 6459 fcntl(SOCK, F_SETFD, FD_CLOEXEC); 6460 }; 6461 6462 # Run the wrapper script by evaling it 6463 if ($cmd =~ /\/([^\/]+)\/([^\/]+)$/) { 6464 $pkg = $1; 6465 } 6466 $0 = $cmd; 6467 @ARGV = @$argv; 6468 $main_process_id = $$; 6469 eval " 6470 \%pkg::ENV = \%ENV; 6471 package $pkg; 6472 do \"$cmd\"; 6473 die \$@ if (\$@); 6474 "; 6475 if ($@) { 6476 print STDERR "Perl failure : $@\n"; 6477 } 6478 exit(0); 6479 } 6480return $pid; 6481} 6482 6483# canonicalize_ip6(address) 6484# Converts an address to its full long form. Ie. 2001:db8:0:f101::20 to 6485# 2001:0db8:0000:f101:0000:0000:0000:0020 6486sub canonicalize_ip6 6487{ 6488my ($addr) = @_; 6489return $addr if (!&check_ip6address($addr)); 6490my @w = split(/:/, $addr); 6491my $idx = &indexof("", @w); 6492if ($idx >= 0) { 6493 # Expand :: 6494 my $mis = 8 - scalar(@w); 6495 my @nw = @w[0..$idx]; 6496 for(my $i=0; $i<$mis; $i++) { 6497 push(@nw, 0); 6498 } 6499 push(@nw, @w[$idx+1 .. $#w]); 6500 @w = @nw; 6501 } 6502foreach my $w (@w) { 6503 while(length($w) < 4) { 6504 $w = "0".$w; 6505 } 6506 } 6507return lc(join(":", @w)); 6508} 6509 6510# expand_ipv6_bytes(address) 6511# Given a canonical IPv6 address, split it into an array of bytes 6512sub expand_ipv6_bytes 6513{ 6514my ($addr) = @_; 6515my @rv; 6516foreach my $w (split(/:/, $addr)) { 6517 $w =~ /^(..)(..)$/ || return ( ); 6518 push(@rv, hex($1), hex($2)); 6519 } 6520return @rv; 6521} 6522 6523sub get_somaxconn 6524{ 6525return defined(&SOMAXCONN) ? SOMAXCONN : 128; 6526} 6527 6528sub is_bad_header 6529{ 6530my ($value, $name) = @_; 6531return $value =~ /^\s*\(\s*\)\s*\{/ ? 1 : 0; 6532} 6533 6534# sysread_line(fh) 6535# Read a line from a file handle, using sysread to get a byte at a time 6536sub sysread_line 6537{ 6538local ($fh) = @_; 6539local $line; 6540while(1) { 6541 local ($buf, $got); 6542 $got = sysread($fh, $buf, 1); 6543 last if ($got <= 0); 6544 $line .= $buf; 6545 last if ($buf eq "\n"); 6546 } 6547return $line; 6548} 6549 6550# getenv(env_key) 6551# Returns env var disregard of case 6552sub getenv 6553{ 6554my ($key) = @_; 6555return $ENV{ uc($key) } || $ENV{ lc($key) }; 6556} 6557