#!/usr/bin/env perl # Program: TinyHTTPD.pl 2.0u alpha - expires March 1, 1999 # Purpose: A fully-functional HTTP/1.0 server that is -*-one Perl-*- script (except optional SSL support) # Disclaimer: Intended for R & D use, not safe for commercial use or public networks - try Apache instead. Don't do e-commerce with this. # Design : Based on a literal interpretation of RFC1945 (HTTP/1.0). # Please notify me of any standards compliance problems. ## How you can help: # - use with actual documents and record detailed bug reports # - compare behavior against RFCs and record detailed bug reports # - examine process code and suggest improvements # - look at To Do's and contribute patches # - add or propose new and useful features ## Old Features from Version 1.4 1994/08/15 (Mr. Titz): # - Supported: HTTP/1.0 GET and POST queries # - Understands: .html and .gif file extensions # - ACLs for remote hosts ## New Features in Version 2.0alpha 1998/08/15 (J. Briggs): # - supports WinNT and Win95 # - Basic Authentication and experimental realm logout feature # - understands If-Modified-Since GET # - understands HEAD query # - understands .html (and .htm), .shtml (and .stm) .gif, .jpg, .bmp, .png file extensions, configurable with %addtypes # - index pages can be enabled/disabled and default index filename set # - directory browsing can be enabled/disabled # - security option $I_AM_PARANOID # - $UPSIZE option to seek and assimilate files of other web servers # - manually configured list of cached files # - virtual CGI directory name is configurable # - should still work in perl4 (except for SSI pos() and $^O), although currently tested with perl5 # - more complete support for CGI environment settings, esp. client request # - complies with intent of RFC robustness principle: "permissive on input, restrictive on output." # - hook functions before and after request, uri translation, response routines # - supports Netscape cookies (not tested with RFC2109) # - file upload is handled by a CGI, using HTTP POST # - SSL works with openSSL 0.9.1c and Net::SSLeay 1.03 (comment out the /proc crap in Net::SSLeay::ssl_read_all for Solaris) - for legal reasons, US users should ensure they have an RSAREF licence. Tested on Solaris. Error on linux is 'missing tmp rsa key' # Here's a shell script to create the server cert and key needed with SSL ### PATH=$PATH:/usr/local/ssl/bin ### ### # SSLeay 0.5.0b+ (21-Dec-95) supports a quick mechanism for generating ### # "dummy" certificates ### cd /usr/local/ssl/certs ### req -new -x509 -nodes -out server.pem -keyout server.pem ### ln -s server.pem `x509 -noout -hash < server.pem ### ### #Then *test* that verify likes the setup ### ### verify /usr/local/ssl/certs/telnetd.pem ## Restrictions: # - CGI programs run as evil shell of some kind, excuse is Win95 # - Win95 cannot do server-push because of backticks presently # - realm auto-logout feature is experimental ## To Do: # - more robust exec of CGI processes (cross-platform is a challenge) # - improve server-parsed HTML (SSI's) week number feature # - add MD5 authentication like Apache? # - detailed and systematic testing ## Author's Notes: # - This program was updated by JB to find a purpose for my obsolete notebook. # - The program style is perl4, in a likely misguided attempt to maintain # backward compatibility. Personally, I prefer perl5 style with my(). # Upgrading to perl5 will be easy. LWP could drastically shorten this program, # but then this program wouldn't consist of one source file. # - Note that pos() does not get localized automatically across subroutines. # - The goals of a server are to be fast and handle multiple connections, # yet the process model in this code can only handle a single connection; # but there are many features that make personal use worthwhile, # especially as other servers get more bloated. ## Credits: # - Thanks to Larry for creating Perl, the universal scripting language. # - Thanks to my maid for freeing up the personal time needed to write this. # - Thanks to Aaron for finding the socket problem with GS NT Perl5.004 98/10/10 ## Start Configuration section BEGIN { $THE_OS = $^O; $USE_SOCKET = eval 'require Socket; Socket->import(qw(SO_REUSEADDR SOCK_STREAM AF_INET PF_INET SOL_SOCKET SOL_SOCKET $CRLF)); 1'; } if ($USE_SOCKET) { $AF_INET = &AF_INET; $PF_INET = &PF_INET; $SOCK_STREAM = &SOCK_STREAM; $SO_REUSEADDR = &SO_REUSEADDR; $SOL_SOCKET = &SOL_SOCKET; } else { # This is the manual configuration way - not recommended # Check if the definitions are correct with /usr/include/sys/socket.h $AF_INET = 2; $PF_INET = $AF_INET; $SOCK_STREAM = 1; $SOCK_STREAM = 2 if $^O eq 'solaris'; if ($THE_OS eq 'MSWin32') { $SO_REUSEADDR = 4; $SOL_SOCKET = 65535; } else { $SO_REUSEADDR = 2; $SOL_SOCKET = 1; } $CRLF = "\015\012"; } $IPPROTO_TCP = getprotobyname('tcp'); # GS Perl5.004 needs socket call here, not later socket(S, $PF_INET, $SOCK_STREAM, $IPPROTO_TCP) || die "socket: $!"; if ($THE_OS eq 'MSWin32') { if ($ENV{'OS'} eq 'Windows_NT') { $THE_OS = 'WINNT'; } else { $THE_OS = 'WIN95'; } } if ($THE_OS eq 'WINNT') { $HOSTNAME = $ENV{'COMPUTERNAME'}; # or hostname ? } elsif ($THE_OS eq 'WIN95') { $HOSTNAME = `net config`; ($HOSTNAME) = $HOSTNAME =~ s/\\\\(\w+)/$1/; } else { $HOSTNAME = `/bin/hostname`; # or uname -n ? chop $HOSTNAME; } $HOSTNAME =~ tr/A-Z/a-z/; # enable or disable auto-configuration of document and CGI directories # this works with Netscape Enterprise, IIS, and OmniHTTPD directories $UPSIZE = 0; # SSL $ssl_config = 0; # 1=SSL on, 0=SSL off $cert_pem = 'server.pem'; # server certificate file name $key_pem = 'server.pem'; # server key file name # physical stuff $port = 8000; # Port to listen on $htmldir = '/netscape/server/docs'; # Base directory for HTML files $cgidir = '/netscape/server/cgi-bin'; # Base dir. for CGI # virtual stuff - for Windoze users, make lowercase $htmlprefix = ''; # virtual doc path, no trailing / $cgiprefix = '/scripts'; # virtual script path, no leading / $perlpath = $^X || '/perl/bin/perl.exe'; # no shebang on Windoze $accesslogfile = 'access_log'; $errorlogfile = 'error_log'; $debuglogfile = 'debug_log'; # email for server administrator $server_admin = 'root@localhost'; # enable or disable Security Paranoia Option $I_AM_PARANOID = 0; # expire browser login seconds $AUTO_LOGOUT_INTERVAL = 60; # enable or disable directory listing if no index page $OK_LIST_DIRECTORIES = 1; # enable or disable pathinfo processing $OK_PATHINFO = 1; # enable or disable SSIs (works on static content, could do it on cgi, too!) $OK_SSI = 1; $SSI_MAX_NESTING_LEVEL=4; # enable or disable CGIs $OK_CGI = 1; $OK_QUERY = 1; $QUERY_MAX_LEN = 1024; # enable or disable index page if url is a directory name $OK_INDEX_PAGES = 1; $index_page = 'index.html'; # or sometimes 'index.htm' or 'home.html' # enable or disable HTTP/1.0 Server: response (not necessary to send) $OK_SERVER_RESPONSE = 1; # enable or disable HTTP/1.0 Last-Modified: response (not necessary to send) $OK_LAST_MODIFIED_RESPONSE = 1; # Access control %acl= # 'host-pattern', 'url-pattern'. Prefix ! to url means deny ( # 'localhost', '.', ); %login_cache = (); # Basic Authentication Table %auth_tbl_basic = # 'url', 'realm,userid:password,', # do not deviate from this syntax ( # '/', 'J,admin:admin,', ); # Cache most-commonly-used pages, specify virtual path to static pages %page_cache = ( # '/myimage.gif', undef, # '/index.htm', undef, ); # hash of urls to redirect %url_redirect_list = ( # '/myimage.gif', 'http://www.nowhere.com/myimage.gif', ); # hash of urls to move permanently - 301 %url_move_permanently_list = ( # '/myimage.gif', 'http://www.nowhere.com/myimage.gif', ); # hash of urls to move temporarily - 302 %url_move_temporarily_list = ( # '/myimage.gif', 'http://www.nowhere.com/myimage.gif', ); # tell server which file extensions need special content type response headers %addtypes = ( 'htm', 'text/html', 'html', 'text/html', 'shtml','text/html', 'sht', 'text/html', 'stm', 'text/html', 'text', 'text/plain', 'txt', 'text/plain', 'xml', 'text/xml', 'zip', 'application/zip', 'gz', 'application/x-gzip', 'tar', 'application/x-tar', 'sit', 'application/x-stuffit', 'wav', 'audio/x-wav', 'bmp', 'image/bmp', 'gif', 'image/gif', 'jpeg', 'image/jpeg', 'jpg', 'image/jpeg', 'png', 'image/png', 'xbm', 'image/x-xbitmap', 'css', 'text/css', ); # administrator-modified hook functions into web server sub hook_request_pre { # Your code starts here # Your code ends here } sub hook_request_post { # Your code starts here # Your code ends here } sub hook_uri_translate_pre { local($url)=@_; # Your code starts here # Your code ends here $url; } sub hook_uri_translate_post { local($file,$query,$pathinfo,$isascript)=@_; # Your code starts here # Your code ends here ($file,$query,$pathinfo,$isascript); } sub hook_response_pre { local($response); # Your code starts here # Your code ends here $response; } sub hook_response_post { local($response)=@_; # Your code starts here # Your code ends here $response; } do "$0.config"; ## End configuration section # sometimes we don't want headers, like with SSI which nests #$PRINT_HEADERS = 1; $NO_PRINT_HEADERS = 0; local($sigs)=0; local($mysize)=0; sub catch_zap { local($signame) = shift; $sigs++; print ERRORLOG "[@{[scalar localtime]}] ${service}d: SIG: $signame, total sigs=$sigs\n"; if ($signame eq 'INT') { close S; print ERRORLOG "[@{[scalar localtime]}] ${service}d: caught SIGTERM, shutting down\n"; exit; } } $SIG{'INT'} = \&catch_zap; #$SIG{'PIPE'} = \&catch_zap; $SIG{'PIPE'} = 'IGNORE'; # use this for production, please $VERSION = 'TinyHTTPD/2.0'; local($nesting_level)=0; # Note: this makes parse_SSI non-reentrant, ok for now if ($UPSIZE) { if (-d '/netscape/server/docs' && -d '/netscape/server/scripts') { $infestation = 'Netscape'; $htmldir = "/netscape/server/docs"; # Base directory for HTML files $cgidir = "/netscape/server/scripts"; # Base dir. for CGI $htmlprefix = ''; # virtual doc path, no trailing / $cgiprefix = '/scripts'; # virtual script path, no leading / } elsif (-d '/inetpub/wwwroot' && -d '/inetpub/scripts') { $infestation = 'IIS'; $htmldir = "/inetpub/wwwroot"; # Base directory for HTML files $cgidir = "/inetpub/wwwroot/scripts"; # Base dir. for CGI $htmlprefix = ''; # virtual doc path, no trailing / $cgiprefix = '/scripts'; # virtual script path, no leading / } elsif (-d '/home/httpd/html' && -d '/home/httpd/cgi-bin') { $infestation = 'Apache'; $htmldir = "/home/httpd/html"; # Base directory for HTML files $cgidir = "/home/httpd/cgi-bin"; # Base dir. for CGI $htmlprefix = ''; # virtual doc path, no trailing / $cgiprefix = '/cgi-bin'; # virtual script path, no leading / } elsif (-d '/httpd/htdocs' && -d '/httpd/cgi-bin') { $infestation = 'OmniHTTPD'; $htmldir = "/httpd/htdocs"; # Base directory for HTML files $cgidir = "/httpd/cgi-bin"; # Base dir. for CGI $htmlprefix = ''; # virtual doc path, no trailing / $cgiprefix = '/cgi-bin'; # virtual script path, no leading / } else { $infestation = ''; } print "Upsizing... I am 7-of-9 of Borg. $infestation, you will be assimilated.\n" if $infestation; } foreach (keys %ENV) { delete $ENV{$_} unless $_ eq 'PATH'; } if ($I_AM_PARANOID) { $OK_PATHINFO = 0; $OK_SERVER_RESPONSE = 0; $OK_INDEX_PAGES = 0; $OK_SSI = 0; $OK_CGI = 0; $OK_QUERY = 0; print <) && ($< == $>) && ($( == $))) || die "Don't run this program with privileges!\n"; } # set up a server socket, redirect stderr to logfile $sockaddr = 'S n a4 x8'; $this = pack($sockaddr, $AF_INET, $port, "\0\0\0\0"); # socket(S, $PF_INET, $SOCK_STREAM, $IPPROTO_TCP) || die "socket: $!"; setsockopt(S, $SOL_SOCKET, $SO_REUSEADDR, 1); # see Perl Cookbook p. 606 bind(S, $this) || die "bind: $!"; listen(S, 5) || die "listen: $!"; print "Listening to $HOSTNAME on port $port...\n"; open(ACCESSLOG,'>>'.$accesslogfile) || die "$0: cannot open logfile $accesslogfile: $!"; select(ACCESSLOG); $|=1; open(DEBUGLOG,'>>'.$debuglogfile) || die "$0: cannot open logfile $debuglogfile: $!"; select(DEBUGLOG); $|=1; open(ERRORLOG,'>>'.$errorlogfile) || die "$0: cannot open logfile $errorlogfile: $!"; select(ERRORLOG); $|=1; open(STDERR, ">&DEBUGLOG") || die "dup2 log->stderr"; print ERRORLOG "[@{[scalar localtime]}] info: successful server startup\n"; if ($ssl_config) { $service = 'https'; $trace = 3; $ssl = 0; &ssl_start(); } else { $service = 'http'; } local($junk1_undef,$junk2_undef); # accept incoming calls for (;;) { $got = ''; @got = (); local($response) = ''; ($addr = accept(NS,S)) || die "accept: $!"; if ($ssl_config) { &ssl_session_start(); } # ($a,$p,$inetaddr) = unpack($sockaddr, $addr); ($junk1_undef,$junk2_undef,$inetaddr) = unpack($sockaddr, $addr); @inetaddr = unpack('C4', $inetaddr); ($host,$aliases) = gethostbyaddr($inetaddr, $AF_INET); $inetaddr = join(".", @inetaddr); @host = split(' ', "$host $aliases"); $host || do { $host = $inetaddr; }; if ($THE_OS eq 'WIN95') { *STDIN = *NS; # force dup *STDOUT = *NS; } else { open(STDIN, "+<&NS") || die "dup2 ns->stdin: $!"; open(STDOUT, "+>&NS") || die "dup2 ns->stdout: $!"; } if (! $ssl_config) { while () { last if length($_) < 3; push @got, $_; } print DEBUGLOG "\n", @got, "\n"; } select(STDOUT); $|=1; &setenv; $response .= &serve_request; if ($ssl_config) { &ssl_session_end($response); } else { print STDOUT $response; } close(STDIN); close(STDOUT); close(NS); } if ($ssl_config) { &ssl_end; } # Read request from STDIN and produce output sub serve_request { local($content_length) = 0; local($content_type) = ''; local($body) = ''; local($dt_modified_since); local(%logins) = (); # $ENV{'CONTENT-TYPE'} = ''; # $ENV{'CONTENT-LENGTH'} = ''; # # $ENV{'HTTP_HOST'} = ''; # $ENV{'HTTP_USER_AGENT'} = ''; # $ENV{'HTTP_CONNECTION'} = ''; $ENV{'HTTP_ACCEPT'} = ''; $ENV{'HTTP_ACCEPT_CHARSET'} = ''; $ENV{'HTTP_ACCEPT_LANGUAGE'} = ''; $ENV{'HTTP_COOKIE'} = ''; # $ENV{'HTTP_FROM'} = ''; # $ENV{'HTTP_REFERER'} = ''; # # $ENV{'QUERY_STRING'} = ''; # $ENV{'DOCUMENT_ROOT'} = ''; # $ENV{'PATH_INFO'} = ''; # $ENV{'PATH_TRANSLATED'} = ''; # HTTPS [if SET] HTTPS is being used. # HTTPS_CIPHER SSL/TLS cipherspec # SSL_CIPHER The same as HTTPS_CIPHER # SSL_PROTOCOL_VERSION Self explanatory # SSL_SSLEAY_VERSION Self explanatory # HTTPS_KEYSIZE Number of bits in the session key # HTTPS_SECRETKEYSIZE Number of bits in the secret key # SSL_CLIENT_DN DN in client's certificate # SSL_CLIENT_ Component of client's DN # SSL_CLIENT_I_DN DN of issuer of client's certificate # SSL_CLIENT_I_ Component of client's issuer's DN # SSL_SERVER_DN DN in server's certificate # SSL_SERVER_ Component of server's DN # SSL_SERVER_I_DN DN of issuer of server's certificate # SSL_SERVER_I_ Component of server's issuer's DN # SSL_CLIENT_CERT Base64 encoding of client cert # SSL_CLIENT_CERT_CHAIN_n Base64 encoding of client cert chain # Analyze HTTP input. $_ = shift @got; chop; ($method, $url, $proto, $garbage) = split; # very touchy, this split if ($garbage ne '') { print DEBUGLOG "Request2: malformed request $_"; return; } $proto = 'HTTP/0.9' if $proto =~ /^\s*$/; print DEBUGLOG "Request3: method=$method, url=$url, proto=$proto\n"; $url = &uri_unescape($url); # obligatory unescape print DEBUGLOG "Request4: unescape url=$url\n"; $ENV{'REQUEST_URI'} = $url; $ENV{'SERVER_PROTOCOL'} = $proto; $ENV{'REQUEST_METHOD'} = $method; $ENV{'REMOTE_ADDR'} = $inetaddr; $ENV{'REMOTE_USER'} = ''; $ENV{'REMOTE_HOST'} = $ENV{'REMOTE_ADDR'}; $ENV{'SERVER_URL'} = "$service://".$HOSTNAME; $ENV{'SERVER_URL'} .= ':'.$port if $port != 80; print DEBUGLOG "$$ Request5: $method $url\n"; local($headers) = 0; # turn off for HTTP/0.9 if ($proto ne 'HTTP/0.9') { $headers = 1; while ($_ = shift @got) { &parse_request; length || last; # empty line - end of header } if ($content_length) { while ($_ = shift @got) { $body .= $_; last if length($body) >= $content_length; } } foreach ('ACCEPT', 'ACCEPT_CHARSET', 'ACCEPT_LANGUAGE', 'COOKIE') { if ($ENV{'HTTP_'.$_} ne '') { chop $ENV{'HTTP_'.$_}; # chop off ', ' chop $ENV{'HTTP_'.$_}; } } } ($proto =~ m:^HTTP/1\.\d+: || $proto eq 'HTTP/0.9') || do { &print_response(501,$proto); return; }; if ($proto eq 'HTTP/0.9') { ($method eq 'GET') || do { &print_response(501,$method); return; }; } else { ($method=~/^(GET|POST|HEAD)$/) || do { &print_response(501,$method); return; }; } local($file,$query,$pathinfo,$isascript,$status) = &parse_URI($url); if ($status > 0) { print DEBUGLOG "Parse Bad url $url\n"; &print_response($status,$url); return; } &check_ACL($url) || return 0; &check_auth_basic($url) || return 0; if ($method eq 'HEAD') { (-r $file) || do { &print_response(404,$url); return; }; do { &print_response(200,$url); return; }; } if ($OK_LIST_DIRECTORIES && ($file =~ m:/$: || -d $file)) { if ($isascript) { &print_response(404, $url); } else { -d $file || do { &print_response(404, $url); return 0; }; &dirlist($file, $url); } return; } &fetch($file,$query,$pathinfo,$isascript,$headers); } sub parse_request { # We are not complying with the following: # RFC 1945 2.2: HTTP/1.0 headers may be folded onto multiple lines if each # continuation line begins with a space or horizontal tab. # sample code from perlfunc for split, almost works. # $header =~ s/\n\s+/ /g; # fix continuation lines # %hdrs = (UNIX_FROM => split(/^(\S*?):\s*/m, $header)); print DEBUGLOG "Request1: $_"; # $_ = &hook_request_pre($_); s/\n|\r//g; # kill CR and NL chars return '' if length == 0; if (/^Content-Length:[ \t]+(\S*)/i) { $content_length=$1; $ENV{'CONTENT_LENGTH'}=$content_length; } /^Content-Type:[ \t]+(\S*)/i && ($content_type=$1); local($fld_name, $fld_value) = split(/:[ \t]+/); # field names are case-insensitive $fld_name =~ tr/a-z/A-Z/; if ($fld_name eq 'HOST') { $ENV{'HTTP_HOST'} = $fld_value; } elsif ($fld_name eq 'USER-AGENT') { $ENV{'HTTP_USER_AGENT'} = $fld_value; } elsif ($fld_name eq 'PRAGMA') { $ENV{'HTTP_PRAGMA'} = $fld_value; } elsif ($fld_name eq 'CONNECTION') { $ENV{'HTTP_CONNECTION'} = $fld_value; } elsif ($fld_name eq 'ACCEPT') { $ENV{'HTTP_ACCEPT'} .= $fld_value.', '; } elsif ($fld_name eq 'COOKIE') { $ENV{'HTTP_COOKIE'} .= $fld_value.', '; } elsif ($fld_name eq 'ACCEPT-CHARSET') { $ENV{'HTTP_ACCEPT_CHARSET'} .= $fld_value.', '; } elsif ($fld_name eq 'ACCEPT-LANGUAGE') { $ENV{'HTTP_ACCEPT_LANGUAGE'} .= $fld_value.', '; } elsif ($fld_name eq 'FROM') { $ENV{'HTTP_FROM'} = $fld_value; } elsif ($fld_name eq 'REFERER') { $ENV{'HTTP_REFERER'} = $fld_value; } elsif ($fld_name eq 'IF-MODIFIED-SINCE') { $dt_modified_since = &date_parse($fld_value); } elsif ($fld_name eq 'AUTHORIZATION') { # Note that stronger auth schemes appear before BASIC. # if ($fld_value =~ /MD5 (.*?) BASIC/i) { # could do this if ($fld_value =~ /MD5 (.*) BASIC/i) { # could do this # sanity checking on request &print_response(401, 'Authorization'); return; } elsif ($fld_value =~ /BASIC (.*)/i) { local($client_login_string)=$1; $ENV{'AUTH_TYPE'} = 'BASIC'; # ready to receive multiple comma-separated logins # build password hash for this request foreach (split(/,[ \t]+/,$client_login_string)) { ($userid,$pwd) = split(/:/, &b64decode($_)); $logins{$userid} = $pwd; print DEBUGLOG "Req: userid=$userid, pwd=$pwd\n"; } } else { print DEBUGLOG "Req: unknown AUTH_TYPE $fld_name\n"; } } else { # unknown request print DEBUGLOG "Bad request type?: $fld_name, $fld_value\n" unless $_ eq ''; } # &hook_request_post(); } sub inlined_pages { # intended for SSI includes, execs, etc. not directly from browser request local($url) = @_; local($file,$query,$pathinfo,$isascript,$status) = &parse_URI($url); &check_ACL($url) || return 0; &check_auth_basic($url) || return 0; # print DEBUGLOG "\n\nParse inlined: calling fetch with $url\n"; local($ret)=&fetch($file,$query,$pathinfo,$isascript,$NO_PRINT_HEADERS); # print DEBUGLOG "Parse: returning fetch with $url\n"; $ret; } sub fetch { local($file,$query,$pathinfo,$isascript,$headers) = @_; local($out) = ''; local($type) = ''; local($mtime) = time; $mysize=0; if ($isascript) { # Execute CGI scripts if ($THE_OS =~ /^WIN/) { (-x $file || -r _ ) || do { &print_response(404,$url) if $headers; return 0; }; } else { (-x $file) || do { &print_response(404,$url) if $headers; return 0; }; } print DEBUGLOG "Executing: $url\n"; # Parsing of sent-back headers is not implemented. # Script effectively talks back to client. local($thiscmd, @thiscmd); if ($THE_OS =~ /^WIN/) { $thiscmd = $perlpath if $file !~ /(\.exe|\.com|\.cmd|\.bat)$/; $thiscmd .= " $file"; $thiscmd =~ s:/:\\:g; $thiscmd .=" $query" if $OK_QUERY && $query ne ''; @thiscmd = ($perlpath, $file); $thiscmd[1] =~ s:/:\\:g; } else { $thiscmd = $file; $thiscmd .= " $query" if $OK_QUERY && $query ne ''; @thiscmd = ($file); } push @thiscmd, $query if $OK_QUERY && $query ne ''; if ($THE_OS eq 'WIN95') { # print $thiscmd, "\n"; $out = `$thiscmd`; } elsif ($method eq 'GET') { if ($THE_OS !~ /^WIN/) { my @ok = (); if (open(GET, "-|")) { while () { #chomp; push(@ok, $_); } close GREP; } else { print DEBUGLOG "safe exec: @thiscmd\n"; exec @thiscmd; } $out = join("", @ok); } else { print DEBUGLOG "unsafe exec: @thiscmd\n"; $out = `$thiscmd`; } } else { $| = 1; print $out; if ($THE_OS !~ /^WIN/) { print DEBUGLOG "system exec: safe pipe @thiscmd\n"; open(PIP, "|-") || exec @thiscmd; } else { print DEBUGLOG "system exec: |$thiscmd\n"; open(PIP, "|$thiscmd") || do { &print_response(500,$url,"pipe: $!"); return; }; } local($x) = select(PIP); $|=1; print DEBUGLOG $body; print $body; close(PIP); select($x); } } else { # Get and return file (-r $file) || do { &print_response(404,$url) if $headers; return 0; }; ($type) = $file =~ /\.(\w+)$/; $type = 'txt' if $type eq '' || ! defined $addtypes{$type}; $mtime = (stat(_))[9]; local($x) = scalar gmtime($mtime); if ($headers) { if ($dt_modified_since) { (!&modified_since(&date_parse($x),$dt_modified_since)) && do { &print_response(304,$url); return 0; }; } } # could cache files needing Location: response header in array if (defined $page_cache{$url}) { print DEBUGLOG "Req: loading $url from cache!\n"; $out = $page_cache{$url}; } else { open(FILE, $file) || do { &print_response(404,$url) if $headers; return 0; }; binmode FILE; # ignored on real OS local($/) = undef; local($_) = ; print DEBUGLOG "SSI: checking file extension for $file of $type\n"; if ($OK_SSI && $type =~ /^(shtml|stm|sht)$/) { print DEBUGLOG "SSI: found $file for SSI\n"; $out = &parse_SSI($_); } else { $out = $_; } close FILE; } } $mysize = length $out; &print_response(200,'','',$addtypes{$type},'',$mtime); return $out; } sub old_check_auth_basic { local($url) = @_; # Check authentication $allow = 1; # by default, no authentication unless configured local($realm); #must iterate over auth_table and see if url is like protected ones local($alldone) = 0; foreach (keys %auth_tbl_basic) { last if $alldone; print DEBUGLOG "Auth: trying $_\n"; # if input url and auth entry doesn't exactly match and doesn't match beginning.'/', skip auth. This covers file items and directory prefixes together. next if ($url !~ m:^$_$:) && ($url !~ m:^$_/:); local($tmp) = $auth_tbl_basic{$_}; print DEBUGLOG "Auth: attempt url = $url, auth=$_\n"; $realm = substr($tmp,0,index($tmp,',')); $tmp = substr($tmp,index($tmp,',')); $allow = 0; foreach (keys %logins) { print DEBUGLOG "Auth: trying combo $_\n"; if ($login_cache{"$_:$url"} || $tmp =~ /,($_:$logins{$_}),/) { print DEBUGLOG "Auth: Success url = $url, login = $_\n"; $allow = 1; $login_cache{$_.':'.$url}++; $ENV{REMOTE_USER} = $_; $alldone = 1; } } } $allow || do {&print_response(401,$url,"Unauthorized",'',$realm) if $headers;return 0}; } sub check_auth_basic { ###### Experimental auto-logout features, maybe bad idea local($url) = @_; return 1 if ! keys %auth_tbl_basic; # Check authentication $allow = 1; # by default, no authentication unless configured local($realm); #must iterate over auth_table and see if url is like protected ones local($tnow) = time; # find the realm local($alldone) = 0; foreach (keys %auth_tbl_basic) { last if $alldone; print DEBUGLOG "Auth: trying :$_: for :$url:\n"; # if input url and auth entry doesn't exactly match and doesn't match beginning.'/', skip auth. This covers file items and directory prefixes together. if (($url =~ m:^$_$:) || ($url =~ m:^$_/:) || ($_ eq '/')) { local($tmp) = $auth_tbl_basic{$_}; print DEBUGLOG "Auth: attempt url = $url, auth=$_\n"; $realm = substr($tmp,0,index($tmp,',')); $tmp = substr($tmp,index($tmp,',')); # password list for realm $allow = 0; local($ick) = $_; # protected file or directory foreach (keys %logins) { print DEBUGLOG "Auth: trying combo $_\n"; print DEBUGLOG "Auth: $tnow -". $login_cache{"$_:$ick"}."\n"; if (exists $login_cache{"$_:$ick"}) { local($myrealm, $mytime) = split(/:/, $login_cache{"$_:$ick"}); print DEBUGLOG "Auth: found a cache time\n"; local($myrealm, $mytime) = split(/:/, $login_cache{"$_:$ick"}); if ($mytime eq "0") { print DEBUGLOG "Auth: 0 for $_ dir $ick realm is $myrealm, time is $mytime at $tnow\n"; print DEBUGLOG "Auth: examining $auth_tbl_basic{$ick} for ($_:$logins{$_})\n"; pos($auth_tbl_basic{$ick})=0; while ($auth_tbl_basic{$ick} =~ /,$_:(.*?),/g) { print DEBUGLOG "Auth: searching auth_tbl_basic, found $1, ",pos($auth_tbl_basic{$ick}),"\n"; if ($1.$myrealm eq $logins{$_}) { print DEBUGLOG "Auth: found a pwd $1\n"; print DEBUGLOG "Auth: ExtRealm Success url = $ick, login = $1\n"; $allow = 1; $login_cache{"$_:$ick"}="$realm:$tnow"; $ENV{'REMOTE_USER'} = $_; $alldone = 1; last; } } $realm = $myrealm; last; } elsif ($tnow - $mytime >= $AUTO_LOGOUT_INTERVAL) { print DEBUGLOG "Auth: LOGOUT for $_ dir $ick realm is $myrealm, time is $mytime at $tnow\n"; $myrealm = $realm.sprintf('%04d',int(rand(10000))); $login_cache{"$_:$ick"} = "$myrealm:0"; print DEBUGLOG "Auth: making realm for $_ : ".$login_cache{"$_:$ick"}."\n"; $realm = $myrealm; last; } else { print DEBUGLOG "Auth: BEFORE LOGOUT for $_ dir $ick realm is $myrealm, time is $mytime at $tnow\n"; pos($auth_tbl_basic{$ick})=0; while ($auth_tbl_basic{$ick} =~ /,$_:(.*?),/g) { print DEBUGLOG "Auth: searching auth_tbl_basic, found $1, ",pos($auth_tbl_basic{$ick}),"\n"; if (($realm eq $myrealm && $1 eq $logins{$_}) || $1.$myrealm eq $logins{$_}) { print DEBUGLOG "Auth: found a pwd $1\n"; print DEBUGLOG "Auth: ExtRealm Success url = $ick, login = $1\n"; $allow = 1; $login_cache{"$_:$ick"}="$myrealm:$tnow"; $ENV{'REMOTE_USER'} = $_; last; } } $realm = $myrealm; last; } } elsif ($tmp =~ /,($_:$logins{$_}),/) { print DEBUGLOG "Auth: fell through on $_:$ick\n"; print DEBUGLOG "Auth: Success url = $ick, login = $_\n"; $allow = 1; $login_cache{"$_:$ick"}="$realm:$tnow"; $ENV{'REMOTE_USER'} = $_; last; } } } } $allow || do {&print_response(401,$url,"Unauthorized",'',$realm) if $headers;return 0}; } sub check_ACL { local($url) = @_; return 1 if ! keys %acl; # Check access control local($allow) = 0; foreach $k (keys %acl) { local($host); foreach $host (@host) { local($acurl, $deny); if ($host =~ /$k/i) { $acurl = $acl{$k}; $deny = ($acurl =~ s/^!//); if ($url =~ /$acurl/) { if ($deny) { do {&print_response(403,$url,'on deny list') if $headers; return 0;}; } else { $allow = 1; } } } } } $allow || do { &print_response(403,$url,'not on allow list') if $headers; return 0 }; } sub parse_URI { # parsing $url is interesting because ... # # - may have query_string # - may have pathinfo # - must be translated from virtual to physical path in a secure manner # - may be a filename # - may be a directory name, translating to index page or dir listing, or not allowed # - may end in slash # - consideration for . and .. and ... # # - MS-DOG considerations? # # - devices: LPTn:{0,2} COMn:?, CON:?, NUL:?, ad nauseum # - case-sensitivity in MS-DOG file systems # - special characters in filenames local($url) = @_; # $url = &hook_uri_translate_pre($url); ($THE_OS =~ /^WIN/) && ($url =~ m;/(LPT\d|COM\d|CON|NUL)[:]{0,2}/*$;i) && return (undef,undef,undef,undef,404); print DEBUGLOG "Parse_URI: url=$url\n"; local($file) = $url; local($query) = ''; local($pathinfo) = ''; local($isascript) = 0; local($tmp); ($file, $query) = split(/\?/, $url, 2) if index($url, '?') >= 0; # prevent directory go-back $file =~ /(\.\.|\.$)/ && return (undef,undef,undef,undef,404); local($junk) = $file; $junk =~ tr/A-Z/a-z/ if $THE_OS =~ /^WIN/; if ($junk =~ /^$cgiprefix/o || (defined $cgisuffix && $junk =~ /$cgisuffix(\?|$)/o)) { $isascript = 1; if ($junk =~ /^$cgiprefix/o) { $file =~ s/^$cgiprefix/$cgidir/io; } else { $file = "$htmldir/$file"; } # zz $query = substr($query, 0, $QUERY_MAX_LEN) if length($query) > $QUERY_MAX_LEN; # safe side print DEBUGLOG "Parse_URI: file=$file\n"; print DEBUGLOG "Parse_URI: query=$query\n"; # pathinfo # keep an eye on this loop for runaway conditions $tmp = $file; while ($tmp && ! -r $tmp) { $pathinfo = substr($tmp, rindex($tmp, '/')) . $pathinfo; $tmp = substr($tmp, 0, rindex($tmp, '/')); } $file = $tmp; # $file = substr($file,0,rindex($file,$pathinfo)) if $pathinfo; ($script_name = $url) =~ s/\?.*$//; print DEBUGLOG "Parse: file2=$file\n"; print DEBUGLOG "Parse: pathinfo=$pathinfo\n"; $ENV{'SCRIPT_NAME'} = $script_name; $ENV{'QUERY_STRING'} = $query if $query; $ENV{'PATH_INFO'} = $pathinfo if $pathinfo; $ENV{'PATH_TRANSLATED'} = $htmldir.$pathinfo if $pathinfo; $ENV{'SCRIPT_FILENAME'} = $file; } elsif ($junk =~ /^$htmlprefix/o) { $file =~ s/^$htmlprefix/$htmldir/io; $file =~ m://: && return (undef,undef,undef,undef,404); if ($OK_INDEX_PAGES && ( (($file =~ m:/$:) && (-r $file.$index_page)) || (-d $file && -r $file.'/'.$index_page)) ) { $file .= $index_page; } } else { print DEBUGLOG "Parse: Invalid url prefix for $file\n"; } # ($file,$query,$pathinfo,$isascript) = &hook_uri_translate_post($file,$query,$pathinfo,$isascript); ($file,$query,$pathinfo,$isascript,0); } sub parse_SSI { local($s) = @_; # input buffer local($out) = ''; # output buffer local($oldpos) = 0; # regex prematch position print DEBUGLOG "Parse_SSI: nesting_level= $nesting_level\n"; if ($nesting_level++ >= $SSI_MAX_NESTING_LEVEL) { print DEBUGLOG "Parse: SSI_MAX_NESTING_LEVEL reached.\n"; $nesting_level--; return ''; } # SSI Configuration Directive states local($errmsg) = '[an error occurred while processing this directive]'; local($sizefmt) = 'bytes'; # domain is (bytes,K) local($timefmt) = '%D %r'; # default is "mm/dd/yyyy hh:mm:ss AM|PM" # times should be formatted according to $timefmt as needed local($localtime) = scalar localtime; local($gmtime) = scalar gmtime; # SSI template like this: # ... but we should also allow parse a list of parameter and values while ($s =~ //g) { local($command, $parameter, $value) = ($1, $2, $3); # print DEBUGLOG pos(), " $command:$parameter:$value\n"; $out .= substr($s, $oldpos, pos($s)-$oldpos-length $&); # $out .= $`; # doesn't seem to work? $oldpos = pos($s); if ($command eq 'config') { # SSI directives if ($parameter eq 'errmsg') { $errmsg = $value; } elsif ($parameter eq 'sizefmt') { $sizefmt = $value if $value eq 'bytes' || $value eq 'abbrev'; } elsif ($parameter eq 'timefmt') { $timefmt = $value; } } elsif ($command eq 'echo') { if ($parameter eq 'var') { if ($value eq 'DOCUMENT_NAME') { $out .= $file; } elsif ($value eq 'DOCUMENT_URI') { $out .= $url; } elsif ($value eq 'QUERY_STRING_UNESCAPED') { $out .= $query; } elsif ($value eq 'DATE_LOCAL') { $out .= fmt_time($localtime, $timefmt); } elsif ($value eq 'DATE_GMT') { $out .= fmt_time($gmtime, $timefmt); } elsif ($value eq 'LAST_MODIFIED') { $out .= fmt_time(scalar localtime((stat $file)[9]), $timefmt); } else { $out .= $ENV{$value} if defined $ENV{$value}; } } } elsif ($command eq 'fsize') { local($parm) = $value; print DEBUGLOG "SSI: fsize file=$value\n"; if ($parameter eq 'file') { # from current directory local($size) = -s $parm; print DEBUGLOG "SSI: fsize size=$size\n"; $size = int($size/1024) if $sizefmt eq 'abbrev'; $out .= $size . ' ' . ($sizefmt eq 'bytes' ? 'bytes' : 'K'); } elsif ($parameter eq 'virtual') { #to do print DEBUGLOG "SSI: fsize file=$value\n"; local($file,$query,$pathinfo,$isascript) = parse_URI($parm); local($size) = -s $file; print DEBUGLOG "SSI: fsize size=$size\n"; $size = int($size/1024) if $sizefmt eq 'abbrev'; $out .= $size . ' ' . ($sizefmt eq 'bytes' ? 'bytes' : 'K'); } } elsif ($command eq 'flastmod') { local($parm)=$value; if ($parameter eq 'file') { # from current directory $out .= fmt_time(scalar localtime((stat $parm)[9]), $timefmt) if -e $parm; } elsif ($parameter eq 'virtual') { local($file,$query,$pathinfo,$isascript) = parse_URI($parm); $out .= fmt_time(scalar localtime((stat $file)[9]), $timefmt) if -r $file; } } elsif ($command eq 'exec') { if ($parameter eq 'cmd') { # physical path $out .= `$value`; } elsif ($parameter eq 'cgi') { # virtual path local($current_pos) = pos($s); # recursion destroys pos() $out .= inlined_pages($value); pos($s) = $current_pos; } } elsif ($command eq 'include') { if ($parameter eq 'file') { # from current directory if (open(INC, $value)) { local($/)=undef; local($sinc) = ; $out .= $sinc; close INC; } else { $out .= "Error: cannot read file $value"; } } elsif ($parameter eq 'virtual') { # relative to server root local($current_pos) = pos($s); # recursion destroys pos() $out .= inlined_pages($value); pos($s) = $current_pos; } } else { $out .= $errmsg; } } $nesting_level--; $out .= substr($s, $oldpos); } sub print_response { # generate error response local($errno, $errgeneral, $errmsg1, $content_type, $realm, $mtime) = @_; local($errmsg) = $errno. ' '.$errors{$errno}; $content_type = 'text/html' if ! defined $content_type || $content_type eq ''; print DEBUGLOG "headers = $headers\n"; # $response = &hook_response_pre(); if ($proto =~ m:^HTTP/1\.\d+:) { $response .= < 200; $authuser = '-'; $authuser = $ENV{'REMOTE_USER'} if $ENV{'REMOTE_USER'} ne ''; local($l) = 0; $l = $mysize if $mysize > 0; print ACCESSLOG < $errmsg

$errmsg

$verrors{$errno}:
$errgeneral

$VERSION
EOD } sub dirlist { local($dir, $url) = @_; opendir(DH, $dir) || do { &print_response(404,$url); return 0; }; local($d) = ''; local($count) = 0; local($url2) = $url; $url = $ENV{SERVER_URL} . $url; $url .= '/' unless substr($url,-1) eq '/'; # Adapted from Plexus dir.pl by Tony Sanders, April 1993 local($url_parent) = $url; ($url_parent) = $url =~ m|($service://.*?/.*?/)[^/]+/$|; # trim off subd $response .= <Index of $url2

Index of $url2

EOD

   $response .= sprintf("%-25s%9s %s\n\n", 'Modified', 'Size', 'Description');

   $response .= sprintf("%35sParent Directory\n",
           '', $count++, $url_parent, (length($url_parent)?'':'/'));

   # Get directory listing
   local(@dirs) = sort(readdir(DH));

   while ($_ = shift @dirs) {
	next if /^\.+$/;

	local($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
	    $atime, $mtime, $ctime, $blksize, $blocks);

	($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
	    $atime, $mtime, $ctime, $blksize, $blocks) = stat("$dir/$_");

	$response .= scalar gmtime($mtime);

	if (-d _) {
	   $response .= '     [DIR] ';
           $response .= "$_\n";
	}
	else {
	   $response .= sprintf(" %9d ", $size);
           $response .= "$_\n";
	}
       
	$count++;
   }

   closedir(DH);
   1
}

sub uri_init {
   %subst = ();  # compiled patterns

   # Build a char->hex map
   for (0..255) {
       $escapes{chr($_)} = sprintf("%%%02X", $_);
   }
}

sub uri_escape {
# from CPAN
    local($text, $patn) = @_;
    return undef unless defined $text;

    if (defined $patn) {
       unless (exists  $subst{$patn}) {
	  # Because we can't compile regex we fake it with a cached sub
	  $subst{$patn} =
	      eval "sub {\$_[0] =~ s/([$patn])/\$escapes{\$1}/g; }";
	  print DEBUGLOG "uri_escape: $@" if $@;
       }
       &{$subst{$patn}}($text);
    }
    else {
       # Default unsafe characters. (RFC1738 section 2.2)
       $text =~ s/([\x00-\x20\"#%;<>?{}|\\^~`\[\]\x7F-\xFF])/$escapes{$1}/g;
    }

    $text
}

sub uri_unescape {
# from CPAN
    # Note from RFC1630:  "Sequences which start with a percent sign
    # but are not followed by two hexadecimal characters are reserved
    # for future extension"
    local(@copy) = @_;
    for (@copy) { s/%([\dA-Fa-f]{2})/chr(hex($1))/eg; }
    wantarray ? @copy : $copy[0];
}

sub b64init {
# Mark Mielke 
   INITIALIZE: {
      @Table = (('A' .. 'Z'), ('a' .. 'z'), ('0' .. '9'), '+', '/');
      for ($_ = 0; $_ <= $#Table; $_++) {
          $DecodeTable[ord($Table[$_])] = $_;
      }
   }
}

sub b64encode {
# Mark Mielke 

   local($_) = unpack('B*', $_[0]);
   $_ .= '0' x (6 - (length($_) % 6)) if (length($_) % 6) != 0;
   s/.{6}/$Table[ord(pack('B6', $&)) >> 2]/eg;
   $_
}

sub b64decode {
# Mark Mielke 

   local($_) = $_[0];
   s/./unpack('B6', chr($DecodeTable[ord($&)] << 2))/eg;
   $_ = pack('B' . (int(length($_) / 8) * 8), $_);

   s/\000//g; # sometimes extra nulls
   $_
}

sub dtinit {

   %months = ('Jan' , 0, 'Feb' , 1, 'Mar' , 2, 'Apr' , 3, 'May' , 4, 'Jun' , 5,
              'Jul' , 6, 'Aug' , 7, 'Sep' , 8, 'Oct' , 9, 'Nov' , 10, 'Dec' , 11);

   %month_abbrev_from_num = ('0', 'Jan' , 1, 'Feb' , 2, 'Mar' , 3, 'Apr' , 4, 'May' , 5, 'Jun' , 6, 'Jul' , 7, 'Aug' , 8, 'Sep' , 9, 'Oct' , 10, 'Nov' , 11, 'Dec');

   %day_num_from_abbrev = ('Sun' , 0, 'Mon' , 1, 'Tue' , 2, 'Wed' , 3, 'Thu' , 4, 'Fri' , 5, 'Sat', 6 );

   %day_name_from_abbrev = ( 'Sun', 'Sunday',
                             'Mon', 'Monday',
     	                     'Tue', 'Tuesday',
     	                     'Wed', 'Wednesday',
	                     'Thu', 'Thursday',
	                     'Fri', 'Friday',
	                     'Sat', 'Saturday' );

   %month_name_from_abbrev = ( 'Jan', 'January',
                    'Feb', 'February',
                    'Mar', 'March',
                    'Apr', 'April',
                    'May', 'May',
                    'Jun', 'June',
                    'Jul', 'July',
                    'Aug', 'August',
                    'Sep', 'September',
                    'Oct', 'October',
                    'Nov', 'November',
                    'Dec', 'December' );
}

## Various date formats commonly used in internet software
## Sun, 06 Nov 1994 08:49:37 GMT    ; RFC 822, updated by RFC 1123
## Sunday, 06-Nov-94 08:49:37 GMT   ; RFC 850, obsoleted by RFC 1036
## Sun Nov  6 08:49:37 1994         ; ANSI C's asctime() format

sub date_parse {
   local($d) = @_;

   return '' if $d eq '';

   local($year, $month, $day, $hour, $min, $sec);

# RFC 822, updated by RFC 1123
   if ($d =~ /^[a-zA-Z]{3}, (\d{2}) ([a-zA-Z]{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/) {
      $day  = $1;
      $month = $months{$2}+1;
      $year = $3;

      $hour = $4; $min  = $5; $sec  = $6;
   }
# RFC 850, obsoleted by RFC 1036
   elsif ($d =~ /^[a-zA-Z]{6,}, (\d{2})-([a-zA-Z]{3})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/) {
      $day  = $1;
      $month = $months{$2}+1;
      $year = 1900+$3;

      $hour = $4; $min = $5; $sec = $6;
   }
# ANSI C's asctime() format
   elsif ($d =~ /^[a-zA-Z]{3} ([a-zA-Z]{3}) +(\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/) {
      $day  = $2;
      $month = $months{$1}+1;
      $year = $6;

      $hour = $3; $min = $4; $sec = $5;
   }
   else {
      return '';
   }

   # generate datestamp like Date::Manip uses for comparisons

    if ($year  >= 1970 && $year  <= 9999 &&
       $month >=    1 && $month <=   12 &&
       $day   >=    1 && $day   <=   31 &&
       $hour  >=    0 && $hour  <=   24 &&
       $min   >=    0 && $min   <=   59 &&
       $sec   >=    0 && $sec   <=   59 ) {

       sprintf("%4d%02d%02d:%02d%02d%02d",$year,$month,$day,$hour,$min,$sec);
    }
    else {
       '';
    }
}

sub modified_since {
# perform date comparison for If-Modified-Since client request
   local($date_modified_since, $date_file) = @_;

   return 1 if $date_modified_since lt $date_file;

   0
}

## Sun, 06 Nov 1994 08:49:37 GMT    ; RFC 822, updated by RFC 1123
sub rfc_date {
   local($date) = @_;

   if (local($wday, $month, $day, $time, $year) = $date =~ 
      /(\w+) (\w+) +(\d+) (\d{2}:\d{2}:\d{2}) (\d{4})/) {
      sprintf("%s, %02d %s %d %s GMT",$wday,$day,$month,$year,$time);
   }
   else {
      ''
   }
}

sub fmt_time {
   local($date, $fmt) = @_;

   ##### Note: consider accepting numeric time instead of string
   ##### for easier calculation of weeknumber and daynumber

   local($wday, $month, $day, $hour, $min, $sec, $year) = $date =~ 
         /(\w+) (\w+) +(\d+) (\d{2}):(\d{2}):(\d{2}) (\d{4})/;

   local($out)    = '';
   local($oldpos) = 0;

   while ($fmt =~ /(%[a-zA-Z])/g) {
      local($f) = $1;
      $out .= substr($fmt, $oldpos, pos($fmt)-$oldpos-length $f);
      $oldpos = pos($fmt);
#      print DEBUGLOG 'fmt_time: ',pos($fmt), " $f\n";

      # Date as "%m/%d/%y"
      if ($f eq '%D') { $out .= "@{[sprintf('%02d',$months{$month}+1)]}/$day/$year"}
      # time as "%I:%M:%S %p"
      elsif ($f eq '%r') { $out .= "$hour:$min:$sec ".($hour >= 12 ? 'PM' : 'AM')}
      # Day of the week abbreviation
      elsif ($f eq '%a') { $out .= $wday}
      # Day of the week
      elsif ($f eq '%A') { $out .= $day_name_from_abbrev{$wday}}
      # Month name abbreviation
      elsif ($f eq '%b') { $out .= $month}
      # Month name
      elsif ($f eq '%B') { $out .= $month_name_from_abbrev{$month}}
      # Date like '01'
      elsif ($f eq '%d') { $out .= sprintf("%02d",$day)}
      # Date like '1'
      elsif ($f eq '%e') { $out .= sprintf("%d",$day)}
      # 24-hour clock hour
      elsif ($f eq '%H') { $out .= sprintf("%02d",$hour)}
      # 12-hour clock hour
      elsif ($f eq '%I') { $out .= sprintf("%02d", $hour > 12 ? $hour - 12 : $hour)}
      # Decimal day of the year
      elsif ($f eq '%j') {
# To Do
         $out .= '';
      }
      # Month number
      elsif ($f eq '%m') { $out .= sprintf("%02d",$months{$month})}
      # Minutes
      elsif ($f eq '%M') { $out .= sprintf("%02d",$min)}
      # AM | PM
      elsif ($f eq '%p') { $out .= $hour >= 12 ? 'PM' : 'AM'}
      # Seconds
      elsif ($f eq '%S') { $out .= $sec}
      # 24-hour time as "%H:%M:%S"
      elsif ($f eq '%T') { $out .= "$hour:$min:$sec"}
      # Week of the year 
      elsif ($f eq '%U' || $f eq '%W') {
# To Do
         $out .= ''
      }
      # Day of the week number
      elsif ($f eq '%w') { $out .= $day_num_from_abbrev{$wday}}
      # Year of the century
      elsif ($f eq '%y') { $out .= sprintf("%02d",substr($year, -2))}
      # Year
      elsif ($f eq '%Y') { $out .= $year}
      # Time Zone
      elsif ($f eq '%Z') { $out .= $ENV{'TZ'}}
      else { $out .= $f}
   }

   $out .= substr($fmt, $oldpos);
}

sub page_cache_init {
    local($/) = undef;

    foreach (keys %page_cache) {
       local($file,$query,$pathinfo,$isascript) = parse_URI($_);

       open(X, $file) || die "$0: cannot open cache file $_: $!";
       binmode X;
       $page_cache{$_} = ;
       close X;
    }
    1
}

sub setenv {
   foreach (keys %ENV) {
      delete $ENV{$_} unless $_ =~ /^(PATH|SERVER_SOFTWARE|SERVER_ADMIN|GATEWAY_INTERFACE|SERVER_PORT|SERVERNAME_DOCUMENT_ROOT|HTTPS)$/;
   }
}

sub check_redirect {
   local($url) = @_;

   if (defined $url_redirect_list{$url}) {
      print_response(301, '', '', '', '', '', $url);
      1;
   }
   else {
      0;
   }
}

sub check_move_permanently {
   local($url) = @_;

   if (defined $url_move_permanently_list{$url}) {
      print_response(301, '', '', '', '', '', $url);
      1;
   }
   else {
      0;
   }
}

sub check_move_temporarily {
   local($url) = @_;

   if (defined $url_move_temporarily_list{$url}) {
      print_response(302, '', '', '', '', '', $url);
      1;
   }
   else {
      0;
   }
}

sub logdate {
# according to Stefan Stapelberg:
#
# Common Logfile Format (CLF):  dns RFC931-inetd authuserid [date] "clf-uri" resp-code req-size
# Combined Logfile Format:      CLF + "referrer" + "user-agent"
# Extended Logfile Format:      CLF + user-agent + referrer

    local ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst);
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime;
    local($month) = $month_abbrev_from_num{$mon};
    $year+=1900;$mday++;

    sprintf("%02d/%s/%4d:%02d:%02d:%02d %s",$mday,$month,$year,$hour,$min,$sec,&tz);
}

sub tz {
# Larry Rosler, Usenet posting
   local($l_min, $l_hour, $l_year, $l_yday) = (localtime $^T)[1, 2, 5, 7];
   local($g_min, $g_hour, $g_year, $g_yday) = (   gmtime $^T)[1, 2, 5, 7];
   local($tzval) = ($l_min - $g_min)/60 + $l_hour - $g_hour +
                   24 * ($l_year - $g_year || $l_yday - $g_yday);

   sprintf("%+05d",$tzval);
}
#end

sub ssl_start {
# from sslecho.pl - Echo server using SSL
#
# Copyright (c) 1996,1998 Sampo Kellomaki , All Rights Reserved.
# Date:   27.6.1996, 8.6.1998
#

require Net::SSLeay; import Net::SSLeay qw(die_now die_if_ssl_error);

# use Net::SSLeay qw(die_now die_if_ssl_error);

#  $Net::SSLeay::trace = 3; # Super verbose debugging

#
# Prepare SSLeay
#

   Net::SSLeay::load_error_strings();
   Net::SSLeay::ERR_load_crypto_strings();
   Net::SSLeay::SSLeay_add_ssl_algorithms();
   Net::SSLeay::randomize();

   print DEBUGLOG "ssl: Creating SSL context...\n" if $trace > 1;
   $ctx = Net::SSLeay::CTX_new() || die_now("CTX_new ($ctx): $!\n");
   print DEBUGLOG "ssl: Setting cert and RSA key...\n" if $trace > 1;
   Net::SSLeay::set_server_cert_and_key($ctx, $cert_pem, $key_pem) || die "key";

   $ENV{'SSL_SSLEAY_VERSION'} = '';
}

sub ssl_session_start {
    $old_out = select (NS); $| = 1; select ($old_out);  # Piping hot!
    
    if ($trace) {
	($af,$client_port,$client_ip) = unpack('S n a4 x8',$addr);
	@inetaddr = unpack('C4',$client_ip);
	print DEBUGLOG "$af connection from " . join ('.', @inetaddr)
	    . ":$client_port\n" if $trace;;
    }
    
    #
    # Do SSL negotiation stuff
    #

    print DEBUGLOG "ssl: Creating SSL session (cxt=`$ctx')...\n" if $trace>1;
    $ssl = Net::SSLeay::new($ctx) || die_now("ssl new ($ssl): $!");

    print DEBUGLOG "ssl: Setting fd (ctx $ctx, con $ssl)...\n" if $trace>1;
    Net::SSLeay::set_fd($ssl, fileno(NS));

    print DEBUGLOG "ssl: Entering SSL negotiation phase...\n" if $trace>1;
    
    Net::SSLeay::accept($ssl);
    die_if_ssl_error("ssl_echo: ssl accept: ($!)");
    
    print DEBUGLOG "ssl: Cipher `" . Net::SSLeay::get_cipher($ssl) . "'\n" if $trace;
    
    #
    # Connected. Exchange some data.
    #
    
    $got = Net::SSLeay::ssl_read_all($ssl); # || die "ssl read failed";

    print DEBUGLOG "ssl: got " . length($got) . " bytes\n" if $trace > 2;
    print DEBUGLOG "ssl: Got `$got' (" . length ($got) . " chars)\n" if $trace > 2;

    @got = split /\n/, $got;
}

sub ssl_session_end {
    Net::SSLeay::ssl_write_all($ssl, $response); # ||  die "ssl write failed";
    print DEBUGLOG "ssl: Tearing down the connection.\n\n" if $trace > 1;
    Net::SSLeay::free($ssl);
}

sub ssl_end {
   Net::SSLeay::CTX_free($ctx);
}

__END__