use Socket;
use POSIX;
use Time::Local;
+eval "use Time::HiRes;";
@itoa64 = split(//, "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
}
$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");
%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();
}
}
+ # 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;
}
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() ]);
}
}
die "Failed to open socket : $!";
setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
if (!bind($fh, pack_sockaddr_in($sockets[0]->[1], INADDR_ANY))) {
- print STDERR "Failed to bind to port $sockets[0]->[1] : $!";
+ print STDERR "Failed to bind to port $sockets[0]->[1] : $!\n";
exit(1);
}
listen($fh, SOMAXCONN);
$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);
&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,
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;
($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;
}
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'},
&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");
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");
}
$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);
}
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
&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();
$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 '') {
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) =
}
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
}
}
-# 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
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
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));
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);
}
}
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);
}
}
'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();
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, "");
}
&read_mime_types();
&build_config_mappings();
&read_webmin_crons();
+&precache_files();
if ($config{'session'}) {
dbmclose(%sessiondb);
dbmopen(%sessiondb, $config{'sessiondb'}, 0700);
"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}) {
@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'}) {
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);
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) {
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.
"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;
}
}
}
+# 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
{
}
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'};
+}
+