Handle hostnames with upper-case letters
[webmin.git] / miniserv.pl
index 8ae4f48..2975c84 100755 (executable)
@@ -6,6 +6,7 @@ package miniserv;
 use Socket;
 use POSIX;
 use Time::Local;
+eval "use Time::HiRes;";
 
 @itoa64 = split(//, "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
 
@@ -257,15 +258,25 @@ if ($use_ssl) {
                }
        $client_certs = 0 if (!-r $config{'ca'} || !%certs);
        $ssl_contexts{"*"} = &create_ssl_context($config{'keyfile'},
-                                                $config{'certfile'});
+                                                $config{'certfile'},
+                                                $config{'extracas'});
        foreach $ipkey (@ipkeys) {
-               $ctx = &create_ssl_context($ipkey->{'key'}, $ipkey->{'cert'});
+               $ctx = &create_ssl_context($ipkey->{'key'}, $ipkey->{'cert'},
+                                  $ipkey->{'extracas'} || $config{'extracas'});
                foreach $ip (@{$ipkey->{'ips'}}) {
                        $ssl_contexts{$ip} = $ctx;
                        }
                }
        }
 
+# Load gzip library if enabled
+if ($config{'gzip'} eq '1') {
+       eval "use Compress::Zlib";
+       if (!$@) {
+               $use_gzip = 1;
+               }
+       }
+
 # Setup syslog support if possible and if requested
 if ($use_syslog) {
        open(ERRDUP, ">&STDERR");
@@ -375,6 +386,9 @@ if ($config{'debuglog'}) {
 %webmincron_last = ( );
 &read_file($config{'webmincron_last'}, \%webmincron_last);
 
+# Pre-cache lang files
+&precache_files();
+
 if ($config{'inetd'}) {
        # We are being run from inetd - go direct to handling the request
        &redirect_stderr_to_log();
@@ -402,25 +416,39 @@ if ($config{'inetd'}) {
                        }
                }
 
+       # Work out if IPv6 is being used locally
+       local $sn = getsockname(SOCK);
+       print DEBUG "sn=$sn\n";
+       print DEBUG "length=",length($sn),"\n";
+       $localipv6 = length($sn) > 16;
+       print DEBUG "localipv6=$localipv6\n";
+
        # Initialize SSL for this connection
        if ($use_ssl) {
-               $ssl_con = &ssl_connection_for_ip(SOCK, 0);
+               $ssl_con = &ssl_connection_for_ip(SOCK, $localipv6);
                $ssl_con || exit;
                }
 
        # Work out the hostname for this web server
-       $host = &get_socket_name(SOCK, 0);
+       $host = &get_socket_name(SOCK, $localipv6);
+       print DEBUG "host=$host\n";
        $host || exit;
        $port = $config{'port'};
        $acptaddr = getpeername(SOCK);
+       print DEBUG "acptaddr=$acptaddr\n";
+       print DEBUG "length=",length($acptaddr),"\n";
        $acptaddr || exit;
 
        # Work out remote and local IPs
-       (undef, $peera, undef) = &get_address_ip($acptaddr, 0);
-       (undef, $locala) = &get_socket_ip(SOCK, 0);
+       $ipv6 = length($acptaddr) > 16;
+       print DEBUG "ipv6=$ipv6\n";
+       (undef, $locala) = &get_socket_ip(SOCK, $localipv6);
+       print DEBUG "locala=$locala\n";
+       (undef, $peera, undef) = &get_address_ip($acptaddr, $ipv6);
+       print DEBUG "peera=$peera\n";
 
        print DEBUG "main: Starting handle_request loop pid=$$\n";
-       while(&handle_request($peera, $locala, 0)) { }
+       while(&handle_request($peera, $locala, $ipv6)) { }
        print DEBUG "main: Done handle_request loop pid=$$\n";
        close(SOCK);
        exit;
@@ -465,10 +493,10 @@ foreach $s (split(/\s+/, $config{'sockets'})) {
                }
        elsif ($s =~ /^\*:(\d+)$/) {
                # Listening on all IPs on some port
-               push(@sockets, [ INADDR_ANY, $config{'port'},
+               push(@sockets, [ INADDR_ANY, $1,
                                 PF_INET() ]);
                if ($use_ipv6) {
-                       push(@sockets, [ in6addr_any(), $config{'port'},
+                       push(@sockets, [ in6addr_any(), $1,
                                         Socket6::PF_INET6() ]);
                        }
                }
@@ -851,13 +879,6 @@ while(1) {
                                $SIG{'HUP'} = 'IGNORE';
                                $SIG{'USR1'} = 'IGNORE';
 
-                               # Initialize SSL for this connection
-                               if ($use_ssl) {
-                                       $ssl_con = &ssl_connection_for_ip(
-                                                       SOCK, $ipv6fhs{$s});
-                                       $ssl_con || exit;
-                                       }
-
                                # Close the file handle for the session DBM
                                dbmclose(%sessiondb);
 
@@ -869,6 +890,13 @@ while(1) {
                                &close_all_sockets();
                                close(LISTEN);
 
+                               # Initialize SSL for this connection
+                               if ($use_ssl) {
+                                       $ssl_con = &ssl_connection_for_ip(
+                                                       SOCK, $ipv6fhs{$s});
+                                       $ssl_con || exit;
+                                       }
+
                                print DEBUG
                                  "main: Starting handle_request loop pid=$$\n";
                                while(&handle_request($peera, $locala,
@@ -1429,6 +1457,14 @@ if ($method eq 'POST' &&
        print DEBUG "handle_request: posted_data=$posted_data\n";
        }
 
+# Reject CONNECT request, which isn't supported
+if ($method eq "CONNECT") {
+       &http_error(405, "Method $method is not supported");
+       }
+
+# work out accepted encodings
+%acceptenc = map { $_, 1 } split(/,/, $header{'accept-encoding'});
+
 # replace %XX sequences in page
 $page =~ s/%(..)/pack("c",hex($1))/ge;
 
@@ -1556,7 +1592,8 @@ if ($config{'userfile'}) {
                ($authuser, $authpass) = split(/:/, &b64decode($1), 2);
                print DEBUG "handle_request: doing basic auth check authuser=$authuser authpass=$authpass\n";
                local ($vu, $expired, $nonexist) =
-                       &validate_user($authuser, $authpass, $host);
+                       &validate_user($authuser, $authpass, $host,
+                                      $acptip, $port);
                print DEBUG "handle_request: vu=$vu expired=$expired nonexist=$nonexist\n";
                if ($vu && (!$expired || $config{'passwd_mode'} == 1)) {
                        $authuser = $vu;
@@ -1624,7 +1661,8 @@ if ($config{'userfile'}) {
                                }
 
                        local ($vu, $expired, $nonexist) =
-                               &validate_user($in{'user'}, $in{'pass'}, $host);
+                               &validate_user($in{'user'}, $in{'pass'}, $host,
+                                              $acptip, $port);
                        local $hrv = &handle_login(
                                        $vu || $in{'user'}, $vu ? 1 : 0,
                                        $expired, $nonexist, $in{'pass'},
@@ -1847,7 +1885,7 @@ if ($config{'userfile'}) {
                                &write_data("WWW-authenticate: Basic ".
                                           "realm=\"$config{'realm'}\"\r\n");
                                &write_keep_alive(0);
-                               &write_data("Content-type: text/html\r\n");
+                               &write_data("Content-type: text/html; Charset=iso-8859-1\r\n");
                                &write_data("\r\n");
                                &reset_byte_count();
                                &write_data("<html>\n");
@@ -2104,7 +2142,7 @@ if (-d _) {
        local $resp = "HTTP/1.0 $ok_code $ok_message\r\n".
                      "Date: $datestr\r\n".
                      "Server: $config{server}\r\n".
-                     "Content-type: text/html\r\n";
+                     "Content-type: text/html; Charset=iso-8859-1\r\n";
        &write_data($resp);
        &write_keep_alive(0);
        &write_data("\r\n");
@@ -2193,7 +2231,7 @@ if (&get_type($full) eq "internal/cgi" && $validated != 4) {
                }
        $ENV{"QUERY_STRING"} = $querystring;
        $ENV{"MINISERV_CONFIG"} = $config_file;
-       $ENV{"HTTPS"} = "ON" if ($use_ssl || $config{'inetd_ssl'});
+       $ENV{"HTTPS"} = $use_ssl || $config{'inetd_ssl'} ? "ON" : "";
        $ENV{"MINISERV_PID"} = $miniserv_main_pid;
        $ENV{"SESSION_ID"} = $session_id if ($session_id);
        $ENV{"LOCAL_USER"} = $localauth_user if ($localauth_user);
@@ -2425,24 +2463,67 @@ if (&get_type($full) eq "internal/cgi" && $validated != 4) {
        }
 else {
        # A file to output
-       print DEBUG "handle_request: outputting file\n";
-       open(FILE, $full) || &http_error(404, "Failed to open file");
+       print DEBUG "handle_request: outputting file $full\n";
+       $gzfile = $full.".gz";
+       $gzipped = 0;
+       if ($config{'gzip'} ne '0' && -r $gzfile && $acceptenc{'gzip'}) {
+               # Using gzipped version
+               @stopen = stat($gzfile);
+               if ($stopen[9] >= $stfull[9] && open(FILE, $gzfile)) {
+                       print DEBUG "handle_request: using gzipped $gzfile\n";
+                       $gzipped = 1;
+                       }
+               }
+       if (!$gzipped) {
+               # Using original file
+               @stopen = @stfull;
+               open(FILE, $full) || &http_error(404, "Failed to open file");
+               }
        binmode(FILE);
+
+       # Build common headers
        local $resp = "HTTP/1.0 $ok_code $ok_message\r\n".
                      "Date: $datestr\r\n".
                      "Server: $config{server}\r\n".
                      "Content-type: ".&get_type($full)."\r\n".
-                     "Content-length: $stfull[7]\r\n".
-                     "Last-Modified: ".&http_date($stfull[9])."\r\n".
-                     "Expires: ".&http_date(time()+$config{'expires'})."\r\n";
-       &write_data($resp);
-       $rv = &write_keep_alive();
-       &write_data("\r\n");
-       &reset_byte_count();
-       while(read(FILE, $buf, 1024) > 0) {
-               &write_data($buf);
+                     "Last-Modified: ".&http_date($stopen[9])."\r\n".
+                     "Expires: ".
+                       &http_date(time()+&get_expires_time($simple))."\r\n";
+
+       if (!$gzipped && $use_gzip && $acceptenc{'gzip'} &&
+           &should_gzip_file($full)) {
+               # Load and compress file, then output
+               print DEBUG "handle_request: outputting gzipped file $full\n";
+               open(FILE, $full) || &http_error(404, "Failed to open file");
+               {
+                       local $/ = undef;
+                       $data = <FILE>;
+               }
+               close(FILE);
+               @stopen = stat($file);
+               $data = Compress::Zlib::memGzip($data);
+               $resp .= "Content-length: ".length($data)."\r\n".
+                        "Content-Encoding: gzip\r\n";
+               &write_data($resp);
+               $rv = &write_keep_alive();
+               &write_data("\r\n");
+               &reset_byte_count();
+               &write_data($data);
+               }
+       else {
+               # Stream file output
+               $resp .= "Content-length: $stopen[7]\r\n";
+               $resp .= "Content-Encoding: gzip\r\n" if ($gzipped);
+               &write_data($resp);
+               $rv = &write_keep_alive();
+               &write_data("\r\n");
+               &reset_byte_count();
+               my $bufsize = $config{'bufsize'} || 1024;
+               while(read(FILE, $buf, $bufsize) > 0) {
+                       &write_data($buf);
+                       }
+               close(FILE);
                }
-       close(FILE);
        }
 
 # log the request
@@ -2474,7 +2555,7 @@ else {
        &write_data("HTTP/1.0 $_[0] $_[1]\r\n");
        &write_data("Server: $config{server}\r\n");
        &write_data("Date: $datestr\r\n");
-       &write_data("Content-type: text/html\r\n");
+       &write_data("Content-type: text/html; Charset=iso-8859-1\r\n");
        &write_keep_alive(0);
        &write_data("\r\n");
        &reset_byte_count();
@@ -2769,7 +2850,8 @@ while(($idx = index($main::read_buffer, "\n")) < 0) {
                $more = Net::SSLeay::read($ssl_con);
                }
        else {
-                local $ok = sysread(SOCK, $more, 1024);
+               my $bufsize = $config{'bufsize'} || 1024;
+                local $ok = sysread(SOCK, $more, $bufsize);
                $more = undef if ($ok <= 0);
                }
        if ($more eq '') {
@@ -3185,12 +3267,12 @@ sub urlize {
   return $tmp2;
 }
 
-# validate_user(username, password, host)
+# validate_user(username, password, host, remote-ip, webmin-port)
 # Checks if some username and password are valid. Returns the modified username,
 # the expired / temp pass flag, and the non-existence flag
 sub validate_user
 {
-local ($user, $pass, $host) = @_;
+local ($user, $pass, $host, $actpip, $port) = @_;
 return ( ) if (!$user);
 print DEBUG "validate_user: user=$user pass=$pass host=$host\n";
 local ($canuser, $canmode, $notexist, $webminuser, $sudo) =
@@ -3242,7 +3324,7 @@ elsif ($canmode == 1) {
        }
 elsif ($canmode == 2 || $canmode == 3) {
        # Attempt PAM or passwd file authentication
-       local $val = &validate_unix_user($canuser, $pass);
+       local $val = &validate_unix_user($canuser, $pass, $acptip, $port);
        print DEBUG "validate_user: unix val=$val\n";
        if ($val && $sudo) {
                # Need to check if this Unix user can sudo
@@ -3268,7 +3350,7 @@ else {
        }
 }
 
-# validate_unix_user(user, password)
+# validate_unix_user(user, password, remote-ip, local-port)
 # Returns 1 if a username and password are valid under unix, 0 if not,
 # or 2 if the account has expired.
 # Checks PAM if available, and falls back to reading the system password
@@ -3283,6 +3365,8 @@ if ($use_pam) {
        local $pamh = new Authen::PAM($config{'pam'}, $pam_username,
                                      \&pam_conv_func);
        if (ref($pamh)) {
+               $pamh->pam_set_item("PAM_RHOST", $_[2]) if ($_[2]);
+               $pamh->pam_set_item("PAM_TTY", $_[3]) if ($_[3]);
                local $pam_ret = $pamh->pam_authenticate();
                if ($pam_ret == PAM_SUCCESS()) {
                        # Logged in OK .. make sure password hasn't expired
@@ -3504,12 +3588,12 @@ if (!$uinfo) {
        return ( undef, 0, 1, undef ) if (!@uinfo && !$pamany);
 
        if (@uinfo) {
-               if (defined(@allowusers)) {
+               if (scalar(@allowusers)) {
                        # Only allow people on the allow list
                        return ( undef, 0, 0, undef )
                                if (!&users_match(\@uinfo, @allowusers));
                        }
-               elsif (defined(@denyusers)) {
+               elsif (scalar(@denyusers)) {
                        # Disallow people on the deny list
                        return ( undef, 0, 0, undef )
                                if (&users_match(\@uinfo, @denyusers));
@@ -3632,9 +3716,14 @@ return $get_socket_name_cache{$myaddr};
 sub run_login_script
 {
 if ($config{'login_script'}) {
-       system($config{'login_script'}.
-              " ".join(" ", map { quotemeta($_) || '""' } @_).
-              " >/dev/null 2>&1 </dev/null");
+       alarm(5);
+       $SIG{'ALRM'} = sub { die "timeout" };
+       eval {
+               system($config{'login_script'}.
+                      " ".join(" ", map { quotemeta($_) || '""' } @_).
+                      " >/dev/null 2>&1 </dev/null");
+               };
+       alarm(0);
        }
 }
 
@@ -3642,9 +3731,14 @@ if ($config{'login_script'}) {
 sub run_logout_script
 {
 if ($config{'logout_script'}) {
-       system($config{'logout_script'}.
-              " ".join(" ", map { quotemeta($_) || '""' } @_).
-              " >/dev/null 2>&1 </dev/null");
+       alarm(5);
+       $SIG{'ALRM'} = sub { die "timeout" };
+       eval {
+               system($config{'logout_script'}.
+                      " ".join(" ", map { quotemeta($_) || '""' } @_).
+                      " >/dev/null 2>&1 </dev/null");
+               };
+       alarm(0);
        }
 }
 
@@ -4060,16 +4154,17 @@ foreach $k (keys %{$_[0]}) {
                                 'key' => $_[0]->{$k},
                                 'index' => scalar(@rv) };
                $ipkey->{'cert'} = $_[0]->{'ipcert_'.$1};
+               $ipkey->{'extracas'} = $_[0]->{'ipextracas_'.$1};
                push(@rv, $ipkey);
                }
        }
 return @rv;
 }
 
-# create_ssl_context(keyfile, [certfile])
+# create_ssl_context(keyfile, [certfile], [extracas])
 sub create_ssl_context
 {
-local ($keyfile, $certfile) = @_;
+local ($keyfile, $certfile, $extracas) = @_;
 local $ssl_ctx;
 eval { $ssl_ctx = Net::SSLeay::new_x_ctx() };
 $ssl_ctx ||= Net::SSLeay::CTX_new();
@@ -4080,9 +4175,8 @@ if ($client_certs) {
        Net::SSLeay::CTX_set_verify(
                $ssl_ctx, &Net::SSLeay::VERIFY_PEER, \&verify_client);
        }
-if ($config{'extracas'}) {
-       local $p;
-       foreach $p (split(/\s+/, $config{'extracas'})) {
+if ($extracas && $extracas ne "none") {
+       foreach my $p (split(/\s+/, $extracas)) {
                Net::SSLeay::CTX_load_verify_locations(
                        $ssl_ctx, $p, "");
                }
@@ -4155,6 +4249,7 @@ sub reload_config_file
 &read_mime_types();
 &build_config_mappings();
 &read_webmin_crons();
+&precache_files();
 if ($config{'session'}) {
        dbmclose(%sessiondb);
        dbmopen(%sessiondb, $config{'sessiondb'}, 0700);
@@ -4201,10 +4296,11 @@ my %vital = ("port", 80,
          "maxconns", 50,
          "pam", "webmin",
          "sidname", "sid",
-         "unauth", "^/unauthenticated/ ^/robots.txt\$ ^[A-Za-z0-9\\-/_]+\\.jar\$ ^[A-Za-z0-9\\-/_]+\\.class\$ ^[A-Za-z0-9\\-/_]+\\.gif\$ ^[A-Za-z0-9\\-/_]+\\.conf\$ ^[A-Za-z0-9\\-/_]+\\.ico\$ ^/robots.txt\$",
+         "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\$",
          "max_post", 10000,
          "expires", 7*24*60*60,
          "pam_test_user", "root",
+         "precache", "lang/en */lang/en",
         );
 foreach my $v (keys %vital) {
        if (!$config{$v}) {
@@ -4717,6 +4813,15 @@ foreach my $d (split(/\s+/, $config{'davpaths'})) {
 @mobile_agents = split(/\t+/, $config{'mobile_agents'});
 @mobile_prefixes = split(/\s+/, $config{'mobile_prefixes'});
 
+# Expires time list
+@expires_paths = ( );
+foreach my $pe (split(/\t+/, $config{'expires_paths'})) {
+       my ($p, $e) = split(/=/, $pe);
+       if ($p && $e ne '') {
+               push(@expires_paths, [ $p, $e ]);
+               }
+       }
+
 # Open debug log
 close(DEBUG);
 if ($config{'debug'}) {
@@ -5080,7 +5185,9 @@ if (!$pid) {
        close(STDIN); close(STDOUT); close(STDERR);
        untie(*STDIN); untie(*STDOUT); untie(*STDERR);
        close($PASSINw); close($PASSOUTr);
-       $( = $uinfo[3]; $) = "$uinfo[3] $uinfo[3]";
+       ($(, $)) = ( $uinfo[3],
+                     "$uinfo[3] ".join(" ", $uinfo[3],
+                                            &other_groups($uinfo[0])) );
        ($>, $<) = ($uinfo[2], $uinfo[2]);
 
        close(SUDOw);
@@ -5115,7 +5222,7 @@ while(<$ptyfh>) {
 close($ptyfh);
 kill('KILL', $pid);
 waitpid($pid, 0);
-local ($ok) = ($out =~ /\(ALL\)\s+ALL/ ? 1 : 0);
+local ($ok) = ($out =~ /\(ALL\)\s+ALL|\(ALL\)\s+NOPASSWD:\s+ALL|\(ALL\s*:\s*ALL\)\s+ALL/ ? 1 : 0);
 
 # Update cache
 if ($PASSINw) {
@@ -5125,6 +5232,19 @@ if ($PASSINw) {
 return $ok;
 }
 
+sub other_groups
+{
+my ($user) = @_;
+my @rv;
+setgrent();
+while(my @g = getgrent()) {
+        my @m = split(/\s+/, $g[3]);
+        push(@rv, $g[2]) if (&indexof($user, @m) >= 0);
+        }
+endgrent();
+return @rv;
+}
+
 # is_mobile_useragent(agent)
 # Returns 1 if some user agent looks like a cellphone or other mobile device,
 # such as a treo.
@@ -5165,19 +5285,24 @@ local @substrings = (
     "iPhone",            # Apple iPhone KHTML browser
     "iPod",              # iPod touch browser
     "MobileSafari",      # HTTP client in iPhone
-    "Android",           # gPhone
     "Opera Mini",        # Opera Mini
     "HTC_P3700",         # HTC mobile device
     "Pre/",              # Palm Pre
     "webOS/",            # Palm WebOS
     "Nintendo DS",       # DSi / DSi-XL
     );
+local @regexps = (
+    "Android.*Mobile",   # Android phone
+    );
 foreach my $p (@prefixes) {
        return 1 if ($agent =~ /^\Q$p\E/);
        }
 foreach my $s (@substrings, @mobile_agents) {
        return 1 if ($agent =~ /\Q$s\E/);
        }
+foreach my $s (@regexps) {
+       return 1 if ($agent =~ /$s/);
+       }
 return 0;
 }
 
@@ -5634,6 +5759,23 @@ foreach my $f (readdir(CRONS)) {
        }
 }
 
+# precache_files()
+# Read into the Webmin cache all files marked for pre-caching
+sub precache_files
+{
+undef(%main::read_file_cache);
+foreach my $g (split(/\s+/, $config{'precache'})) {
+       next if ($g eq "none");
+       foreach my $f (glob("$config{'root'}/$g")) {
+               my @st = stat($f);
+               next if (!@st);
+               $main::read_file_cache{$f} = { };
+               &read_file($f, $main::read_file_cache{$f});
+               $main::read_file_cache_time{$f} = $st[9];
+               }
+       }
+}
+
 # Check if some address is valid IPv4, returns 1 if so.
 sub check_ipaddress
 {
@@ -5694,3 +5836,25 @@ if ($config{'errorlog'} ne '-') {
        }
 select(STDERR); $| = 1; select(STDOUT);
 }
+
+# should_gzip_file(filename)
+# Returns 1 if some path should be gzipped
+sub should_gzip_file
+{
+my ($path) = @_;
+return $path !~ /\.(gif|png|jpg|jpeg|tif|tiff)$/i;
+}
+
+# get_expires_time(path)
+# Given a URL path, return the client-side expiry time in seconds
+sub get_expires_time
+{
+my ($path) = @_;
+foreach my $pe (@expires_paths) {
+       if ($path =~ /$pe->[0]/i) {
+               return $pe->[1];
+               }
+       }
+return $config{'expires'};
+}
+